From d69faf3b763e0628f7260bbd457895a9ac75fd8a Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 09:46:40 +0100 Subject: [PATCH 01/65] update dependencies - sanic 21.6 -> 12.12 - sanic-cors 1.0.0 -> 2.0.0 - rasa-sdk 3.0.4 -> 3.0.5 - sanic-testing 0.7.0 -> 0.8.0 --- poetry.lock | 432 +++++++++++++++++++++++++------------------------ pyproject.toml | 8 +- 2 files changed, 224 insertions(+), 216 deletions(-) diff --git a/poetry.lock b/poetry.lock index edb8d1f6c3db..3e9cdc6b1449 100644 --- a/poetry.lock +++ b/poetry.lock @@ -180,11 +180,11 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "azure-core" -version = "1.21.1" +version = "1.22.1" description = "Microsoft Azure Core Library for Python" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] requests = ">=2.18.4" @@ -267,14 +267,14 @@ numpy = ">=1.15.0" [[package]] name = "boto3" -version = "1.20.45" +version = "1.20.53" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.23.45,<1.24.0" +botocore = ">=1.23.53,<1.24.0" jmespath = ">=0.7.1,<1.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -283,7 +283,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.23.45" +version = "1.23.53" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -370,7 +370,7 @@ python-versions = "*" [[package]] name = "charset-normalizer" -version = "2.0.10" +version = "2.0.11" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -454,7 +454,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "coverage" -version = "6.3" +version = "6.3.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -821,7 +821,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "google-api-core" -version = "2.4.0" +version = "2.5.0" description = "Google API client core library" category = "dev" optional = false @@ -840,7 +840,7 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] [[package]] name = "google-auth" -version = "2.5.0" +version = "2.6.0" description = "Google Authentication Library" category = "main" optional = false @@ -928,7 +928,7 @@ six = "*" [[package]] name = "google-resumable-media" -version = "2.1.0" +version = "2.2.1" description = "Utilities for Google Media Downloads and Resumable Uploads" category = "dev" optional = false @@ -1005,7 +1005,7 @@ numpy = [ [[package]] name = "httpcore" -version = "0.13.7" +version = "0.14.7" description = "A minimal low-level HTTP client." category = "dev" optional = false @@ -1013,11 +1013,13 @@ python-versions = ">=3.6" [package.dependencies] anyio = ">=3.0.0,<4.0.0" +certifi = "*" h11 = ">=0.11,<0.13" sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httptools" @@ -1032,7 +1034,7 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.18.2" +version = "0.21.3" description = "The next generation HTTP client." category = "dev" optional = false @@ -1040,13 +1042,15 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" -httpcore = ">=0.13.3,<0.14.0" +charset-normalizer = "*" +httpcore = ">=0.14.0,<0.15.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotlicffi (>=1.0.0,<2.0.0)"] -http2 = ["h2 (>=3.0.0,<4.0.0)"] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] [[package]] name = "humanfriendly" @@ -1070,7 +1074,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.10.1" +version = "4.11.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -1603,16 +1607,16 @@ python-versions = ">=3.6" [[package]] name = "oauthlib" -version = "3.1.1" +version = "3.2.0" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" category = "main" optional = false python-versions = ">=3.6" [package.extras] -rsa = ["cryptography (>=3.0.0,<4)"] +rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "opt-einsum" @@ -1694,7 +1698,7 @@ test = ["pytest", "pytest-coverage", "mock", "typer-cli"] [[package]] name = "pbr" -version = "5.8.0" +version = "5.8.1" description = "Python Build Reasonableness" category = "dev" optional = false @@ -1713,7 +1717,7 @@ packaging = ">=20.3,<21.0" [[package]] name = "pillow" -version = "9.0.0" +version = "9.0.1" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1760,7 +1764,7 @@ wcwidth = "*" [[package]] name = "protobuf" -version = "3.19.3" +version = "3.19.4" description = "Protocol Buffers" category = "main" optional = false @@ -2205,7 +2209,7 @@ fire = "*" [[package]] name = "rasa-sdk" -version = "3.0.4" +version = "3.0.5" description = "Open source machine learning framework to automate text- and voice-based conversations: NLU, dialogue management, connect to Slack, Facebook, and more - Create chatbots and voice assistants" category = "main" optional = false @@ -2215,8 +2219,8 @@ python-versions = ">=3.7,<3.10" coloredlogs = ">=10,<16" prompt-toolkit = ">=2.0,<3.0" requests = ">=2.23,<3.0" -sanic = ">=21.6.0,<22.0.0" -Sanic-Cors = ">=1.0.0,<2.0.0" +sanic = ">=21.12.0,<22.0.0" +Sanic-Cors = ">=2.0.0,<3.0.0" typing-extensions = ">=3.7.4,<4.0.0" urllib3 = ">=1.26.5,<2.0.0" uvloop = {version = "<0.15.0", markers = "sys_platform != \"win32\""} @@ -2260,7 +2264,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "requests-oauthlib" -version = "1.3.0" +version = "1.3.1" description = "OAuthlib authentication support for Requests." category = "main" optional = false @@ -2361,7 +2365,7 @@ python-versions = ">=3.5" [[package]] name = "s3transfer" -version = "0.5.0" +version = "0.5.1" description = "An Amazon S3 Transfer Manager" category = "main" optional = false @@ -2390,7 +2394,7 @@ tqdm = "*" [[package]] name = "sanic" -version = "21.9.3" +version = "21.12.1" description = "A web server and web framework that's written to go fast. Build fast. Run fast." category = "main" optional = false @@ -2406,22 +2410,22 @@ uvloop = {version = ">=0.5.3", markers = "sys_platform != \"win32\" and implemen websockets = ">=10.0" [package.extras] -all = ["isort (>=5.0.0)", "beautifulsoup4", "chardet (>=3.0.0,<4.0.0)", "pytest-benchmark", "pytest-cov", "pytest-sugar", "m2r2", "pygments", "mypy (>=0.901)", "towncrier", "pytest (==5.2.1)", "uvicorn (<0.15.0)", "coverage (==5.3)", "pytest-sanic", "flake8", "sphinx (>=2.1.2)", "sanic-testing (>=0.7.0)", "bandit", "black", "gunicorn (==20.0.4)", "tox", "sphinx-rtd-theme (>=0.4.3)", "docutils", "types-ujson"] -dev = ["sanic-testing (>=0.7.0)", "pytest (==5.2.1)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901)", "docutils", "pygments", "uvicorn (<0.15.0)", "tox", "towncrier", "types-ujson"] -docs = ["sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)", "docutils", "pygments", "m2r2"] -test = ["sanic-testing (>=0.7.0)", "pytest (==5.2.1)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901)", "docutils", "pygments", "uvicorn (<0.15.0)", "types-ujson"] +all = ["mistune (<2.0.0)", "flake8", "isort (>=5.0.0)", "sanic-testing (>=0.7.0)", "towncrier", "bandit", "m2r2", "mypy (>=0.901,<0.910)", "pytest-cov", "sphinx (>=2.1.2)", "coverage (==5.3)", "pytest-sugar", "cryptography", "tox", "beautifulsoup4", "black", "pytest-benchmark", "docutils", "gunicorn (==20.0.4)", "pytest-sanic", "uvicorn (<0.15.0)", "pygments", "chardet (>=3.0.0,<4.0.0)", "sphinx-rtd-theme (>=0.4.3)", "pytest (==6.2.5)", "types-ujson"] +dev = ["sanic-testing (>=0.7.0)", "pytest (==6.2.5)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901,<0.910)", "docutils", "pygments", "uvicorn (<0.15.0)", "cryptography", "tox", "towncrier", "types-ujson"] +docs = ["sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)", "docutils", "pygments", "m2r2", "mistune (<2.0.0)"] +ext = ["sanic-ext"] +test = ["sanic-testing (>=0.7.0)", "pytest (==6.2.5)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901,<0.910)", "docutils", "pygments", "uvicorn (<0.15.0)", "types-ujson"] [[package]] name = "sanic-cors" -version = "1.0.1" +version = "2.0.1" description = "A Sanic extension adding a decorator for CORS support. Based on flask-cors by Cory Dolphin." category = "main" optional = false python-versions = "*" [package.dependencies] -sanic = ">=21.3.1,<21.6.0 || >21.6.0,<21.9.0 || >21.9.0,<22" -sanic-plugin-toolkit = ">=1.2.0,<2" +sanic = ">=21.9.3" [[package]] name = "sanic-jwt" @@ -2438,17 +2442,6 @@ pyjwt = ">=2.1.0,<2.2.0" all = ["sphinx"] docs = ["sphinx"] -[[package]] -name = "sanic-plugin-toolkit" -version = "1.2.1" -description = "The all-in-one toolkit for creating powerful Sanic Plugins" -category = "main" -optional = false -python-versions = ">=3.7,<4.0" - -[package.dependencies] -sanic = ">=21.3.1,<21.12.0" - [[package]] name = "sanic-routing" version = "0.7.2" @@ -2459,15 +2452,14 @@ python-versions = "*" [[package]] name = "sanic-testing" -version = "0.7.0" +version = "0.8.2" description = "Core testing clients for Sanic" category = "dev" optional = false python-versions = "*" [package.dependencies] -httpx = ">=0.18.0,<0.19.0" -websockets = ">=9.0" +httpx = ">=0.18,<0.22" [[package]] name = "scikit-learn" @@ -2975,7 +2967,7 @@ torch = ["torch (>=1.5.0)"] [[package]] name = "threadpoolctl" -version = "3.0.0" +version = "3.1.0" description = "threadpoolctl" category = "main" optional = false @@ -3018,7 +3010,7 @@ python-versions = ">=3.5" [[package]] name = "towncrier" -version = "21.3.0" +version = "21.9.0" description = "Building newsfiles for your project." category = "dev" optional = false @@ -3029,7 +3021,7 @@ click = "*" click-default-group = "*" incremental = "*" jinja2 = "*" -toml = "*" +tomli = {version = "*", markers = "python_version >= \"3.6\""} [package.extras] dev = ["packaging"] @@ -3159,7 +3151,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "2.27.7" +version = "2.27.9" description = "Typing stubs for requests" category = "dev" optional = false @@ -3170,7 +3162,7 @@ types-urllib3 = "<1.27" [[package]] name = "types-setuptools" -version = "57.4.7" +version = "57.4.9" description = "Typing stubs for setuptools" category = "dev" optional = false @@ -3178,7 +3170,7 @@ python-versions = "*" [[package]] name = "types-urllib3" -version = "1.26.7" +version = "1.26.9" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -3302,7 +3294,7 @@ python-versions = ">=3.7" [[package]] name = "werkzeug" -version = "2.0.2" +version = "2.0.3" description = "The comprehensive WSGI web application library." category = "main" optional = false @@ -3362,7 +3354,7 @@ transformers = ["transformers"] [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.9" -content-hash = "044c48880de7ed95180951147d80f96cfbb9b0c075658287c1fad2a8a9b9f08c" +content-hash = "dd141c2948fcc3bafe63a93b941533089fe5a131701502e060eb709484e7ca1c" [metadata.files] absl-py = [ @@ -3457,8 +3449,8 @@ attrs = [ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] azure-core = [ - {file = "azure-core-1.21.1.zip", hash = "sha256:88d2db5cf9a135a7287dc45fdde6b96f9ca62c9567512a3bb3e20e322ce7deb2"}, - {file = "azure_core-1.21.1-py2.py3-none-any.whl", hash = "sha256:3d70e9ec64de92dfae330c15bc69085caceb2d83813ef6c01cc45326f2a4be83"}, + {file = "azure-core-1.22.1.zip", hash = "sha256:4b6e405268a33b873107796495cec3f2f1b1ffe935624ce0fbddff36d38d3a4d"}, + {file = "azure_core-1.22.1-py3-none-any.whl", hash = "sha256:407381c74e2ccc16adb1f29c4a1b381ebd39e8661bbf60422926d8252d5b757d"}, ] azure-storage-blob = [ {file = "azure-storage-blob-12.8.1.zip", hash = "sha256:eb37b50ddfb6e558b29f6c8c03b0666514e55d6170bf4624e7261a3af93c6401"}, @@ -3495,12 +3487,12 @@ blis = [ {file = "blis-0.7.5.tar.gz", hash = "sha256:833e01e9eaff4c01aa6e049bbc1e6acb9eca6ee513d7b35b5bf135d49705ad33"}, ] boto3 = [ - {file = "boto3-1.20.45-py3-none-any.whl", hash = "sha256:2ea0e0aa1494ef87a342260da8d9381000d774e73d83ee3d7c2906fefcbe2c32"}, - {file = "boto3-1.20.45.tar.gz", hash = "sha256:3d9cb5edeff09598b7065abe5b42affb0b6e1c0c805ab57c051d0f3592a0f02b"}, + {file = "boto3-1.20.53-py3-none-any.whl", hash = "sha256:d96c1aa3edebff6466362e574af6d8a6f2f6c9c1d902d3c7efb0835e09f34ddb"}, + {file = "boto3-1.20.53.tar.gz", hash = "sha256:96f50e95bded5612ac8bd116d5d56a0cce4868870177805cb974d1f4f0a24b73"}, ] botocore = [ - {file = "botocore-1.23.45-py3-none-any.whl", hash = "sha256:793a0a4b572bfb157ba17971e4d783766f59c5a0f117407bbeefeb577efa1ed1"}, - {file = "botocore-1.23.45.tar.gz", hash = "sha256:782323846dad22ea814a64bd64b89c7f04550812d3945ce77748b2bac6fe745b"}, + {file = "botocore-1.23.53-py3-none-any.whl", hash = "sha256:a97834aee61177e11618348ed8fe1963c37f319af628e6f875f1e0fbd587a9ec"}, + {file = "botocore-1.23.53.tar.gz", hash = "sha256:7a628bc8bb2573fbc77709c9e7a02061b750f6ebb8e961562de658eda98e140d"}, ] cachecontrol = [ {file = "CacheControl-0.12.10-py2.py3-none-any.whl", hash = "sha256:b0d43d8f71948ef5ebdee5fe236b86c6ffc7799370453dccb0e894c20dfa487c"}, @@ -3579,8 +3571,8 @@ chardet = [ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, + {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, ] clang = [ {file = "clang-5.0-py2-none-any.whl", hash = "sha256:b9301dff507041b5019b30ae710b78b0552c1ca1d4441b8dfa93c2e85078a5f8"}, @@ -3614,50 +3606,47 @@ colorhash = [ {file = "colorhash-1.0.4.tar.gz", hash = "sha256:5460c5539b68712c97b09ba41e5200c81dce6e23f4533d0f4f8beafc3eb2fafa"}, ] coverage = [ - {file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"}, - {file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"}, - {file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"}, - {file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"}, - {file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"}, - {file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"}, - {file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"}, - {file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"}, - {file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"}, - {file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"}, - {file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"}, - {file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"}, - {file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"}, - {file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"}, - {file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"}, - {file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"}, - {file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"}, - {file = "coverage-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d"}, - {file = "coverage-6.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab"}, - {file = "coverage-6.3.tar.gz", hash = "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, + {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, + {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, + {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, + {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, + {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, + {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, + {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, + {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, + {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, + {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, + {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, ] coveralls = [ {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, @@ -3724,6 +3713,11 @@ deprecated = [ ] dm-tree = [ {file = "dm-tree-0.1.6.tar.gz", hash = "sha256:6776404b23b4522c01012ffb314632aba092c9541577004ab153321e87da439a"}, + {file = "dm_tree-0.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c6185d750ae7078d299262d16f0b8f2ba699a498abc8fe0e213817763796dd5f"}, + {file = "dm_tree-0.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51d3584b6f5c1d2d6f1fbdf3286d92313ecef894294cda3bcf35e49366d135b6"}, + {file = "dm_tree-0.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6780c9bf3f9d388706cf02efd0866c94c85b5330c92a0989bddfdd9878cd0e8"}, + {file = "dm_tree-0.1.6-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:91b895194a1ffc5453d27137c10f03cf9422e43ecb5c1d533f7a24eb46c71143"}, + {file = "dm_tree-0.1.6-cp310-cp310-win_amd64.whl", hash = "sha256:0f8a43ec475776a4e7091ef0ae01e8328f3817f723640a90d31c7acc07e830f7"}, {file = "dm_tree-0.1.6-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a8814a5c838f79e9db22a51369c74f4d92e7f1485ec55d7f665ae4d98478cb4f"}, {file = "dm_tree-0.1.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:e28ba91d3d97230b831716db401ce116ae5c7dcd025161ac16ecb8bd5c870a85"}, {file = "dm_tree-0.1.6-cp36-cp36m-manylinux_2_24_x86_64.whl", hash = "sha256:603392f1a7818a4f43a7033c2061ae7c2085a4a728171b0bbca76bd107fcdfb0"}, @@ -3733,10 +3727,14 @@ dm-tree = [ {file = "dm_tree-0.1.6-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:c4e8d868fc9a75cbdb67e78069b33e62a4c69bc182c1d2adc29ab08e283912d8"}, {file = "dm_tree-0.1.6-cp37-cp37m-win_amd64.whl", hash = "sha256:6d5f64d89f657b11f429e13b1378c8cfbe4baef50e7ab31f3689bfe0cf4a2508"}, {file = "dm_tree-0.1.6-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8d59c5098456667b28c607110537c86c25cbd0ee455f21d033c60ef2d7f48d81"}, + {file = "dm_tree-0.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec003873d759635ba6cc5eabc6da886bc00f6b5ec23a6baffc4fd0218231adad"}, + {file = "dm_tree-0.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a69861be3f2e6d55a86c280e9625ddd9a77f0c9c9936075ad7773bfdfdca70a3"}, {file = "dm_tree-0.1.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:f3bec40e658fe7546c3a56849c743ac9a498e620b3236e82e171801938a56679"}, {file = "dm_tree-0.1.6-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:e87d06478356a2d92c3940dedebcd92a14ad37fba10ebb1839c8140693b83c0a"}, {file = "dm_tree-0.1.6-cp38-cp38-win_amd64.whl", hash = "sha256:02ffa673f20b1756dcf085ef2c354bc59416d843b384c7b71c421f873ffc36c0"}, {file = "dm_tree-0.1.6-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:affc7a6d29442a143c60efb2f03bcb95424d4fe6168f3d31de892c1e601fa0e6"}, + {file = "dm_tree-0.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d02ba486c8768dcb7c2164fa09af118526df9caf71833927d43da9efc1880f9"}, + {file = "dm_tree-0.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e86ae05b002e825680a5da0602376da95caed24ecc54569891cfd3b1503e6786"}, {file = "dm_tree-0.1.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:bd347c74254ba320d57b0e102558c1189e3b4ae1bae952f9aef156b5914567c8"}, {file = "dm_tree-0.1.6-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:8425454192e954692d9a1e0f7b374b3b7030916b17b6055951dc17d58b6fe1b8"}, {file = "dm_tree-0.1.6-cp39-cp39-win_amd64.whl", hash = "sha256:5269183f80f1ae37543a2a30a8f78e4b0460d5da74fb5ac42dc8a476ef8d707e"}, @@ -3815,12 +3813,12 @@ gitpython = [ {file = "GitPython-3.1.26.tar.gz", hash = "sha256:fc8868f63a2e6d268fb25f481995ba185a85a66fcad126f039323ff6635669ee"}, ] google-api-core = [ - {file = "google-api-core-2.4.0.tar.gz", hash = "sha256:ba8787b7c61632cd0340f095e1c036bef9426b2594f10afb290ba311ae8cb2cb"}, - {file = "google_api_core-2.4.0-py2.py3-none-any.whl", hash = "sha256:58e2c1171a3d51778bf4e428fbb4bf79cbd05007b4b44deaa80cf73c80eebc0f"}, + {file = "google-api-core-2.5.0.tar.gz", hash = "sha256:f33863a6709651703b8b18b67093514838c79f2b04d02aa501203079f24b8018"}, + {file = "google_api_core-2.5.0-py2.py3-none-any.whl", hash = "sha256:7d030edbd3a0e994d796e62716022752684e863a6df9864b6ca82a1616c2a5a6"}, ] google-auth = [ - {file = "google-auth-2.5.0.tar.gz", hash = "sha256:6577bbf990ef342a24e12e0c8e9d364af6642acdf206c9045bdb8e039fb4fec9"}, - {file = "google_auth-2.5.0-py2.py3-none-any.whl", hash = "sha256:ee6199b602594c0dcaa00dc3492e62569f24a788f0aca867b989cef444e4a202"}, + {file = "google-auth-2.6.0.tar.gz", hash = "sha256:ad160fc1ea8f19e331a16a14a79f3d643d813a69534ba9611d2c80dc10439dad"}, + {file = "google_auth-2.6.0-py2.py3-none-any.whl", hash = "sha256:218ca03d7744ca0c8b6697b6083334be7df49b7bf76a69d555962fd1a7657b5f"}, ] google-auth-oauthlib = [ {file = "google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a"}, @@ -3885,8 +3883,8 @@ google-pasta = [ {file = "google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed"}, ] google-resumable-media = [ - {file = "google-resumable-media-2.1.0.tar.gz", hash = "sha256:725b989e0dd387ef2703d1cc8e86217474217f4549593c477fd94f4024a0f911"}, - {file = "google_resumable_media-2.1.0-py2.py3-none-any.whl", hash = "sha256:cdc75ea0361e39704dc7df7da59fbd419e73c8bc92eac94d8a020d36baa9944b"}, + {file = "google-resumable-media-2.2.1.tar.gz", hash = "sha256:b1edfb98867c9fa25aa7af12d6468665b83c532b7349effab805a027ea8bbee5"}, + {file = "google_resumable_media-2.2.1-py2.py3-none-any.whl", hash = "sha256:fd616af31b83d48da040c8c09b6994606e1734efb8af9acc97cf5d6070e9ba72"}, ] googleapis-common-protos = [ {file = "googleapis-common-protos-1.54.0.tar.gz", hash = "sha256:a4031d6ec6c2b1b6dc3e0be7e10a1bd72fb0b18b07ef9be7b51f2c1004ce2437"}, @@ -3904,6 +3902,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, @@ -3916,6 +3915,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, @@ -3924,6 +3924,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, @@ -3932,6 +3933,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, @@ -3940,6 +3942,7 @@ greenlet = [ {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, @@ -4010,8 +4013,8 @@ h5py = [ {file = "h5py-3.1.0.tar.gz", hash = "sha256:1e2516f190652beedcb8c7acfa1c6fa92d99b42331cbef5e5c7ec2d65b0fc3c2"}, ] httpcore = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, ] httptools = [ {file = "httptools-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e"}, @@ -4040,8 +4043,8 @@ httptools = [ {file = "httptools-0.3.0.tar.gz", hash = "sha256:3f9b4856d46ba1f0c850f4e84b264a9a8b4460acb20e865ec00978ad9fbaa4cf"}, ] httpx = [ - {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, - {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"}, + {file = "httpx-0.21.3-py3-none-any.whl", hash = "sha256:df9a0fd43fa79dbab411d83eb1ea6f7a525c96ad92e60c2d7f40388971b25777"}, + {file = "httpx-0.21.3.tar.gz", hash = "sha256:7a3eb67ef0b8abbd6d9402248ef2f84a76080fa1c839f8662e6eb385640e445a"}, ] humanfriendly = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, @@ -4052,8 +4055,8 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, - {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, + {file = "importlib_metadata-4.11.0-py3-none-any.whl", hash = "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad"}, + {file = "importlib_metadata-4.11.0.tar.gz", hash = "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f"}, ] incremental = [ {file = "incremental-21.3.0-py2.py3-none-any.whl", hash = "sha256:92014aebc6a20b78a8084cdd5645eeaa7f74b8933f70fa3ada2cfbd1e3b54321"}, @@ -4524,8 +4527,8 @@ numpy = [ {file = "numpy-1.19.5.zip", hash = "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4"}, ] oauthlib = [ - {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"}, - {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, + {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"}, + {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"}, ] opt-einsum = [ {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"}, @@ -4552,46 +4555,49 @@ pathy = [ {file = "pathy-0.6.1.tar.gz", hash = "sha256:838624441f799a06b446a657e4ecc9ebc3fdd05234397e044a7c87e8f6e76b1c"}, ] pbr = [ - {file = "pbr-5.8.0-py2.py3-none-any.whl", hash = "sha256:176e8560eaf61e127817ef93d8a844803abb27a4d4637f0ff3bb783129be2e0a"}, - {file = "pbr-5.8.0.tar.gz", hash = "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf"}, + {file = "pbr-5.8.1-py2.py3-none-any.whl", hash = "sha256:27108648368782d07bbf1cb468ad2e2eeef29086affd14087a6d04b7de8af4ec"}, + {file = "pbr-5.8.1.tar.gz", hash = "sha256:66bc5a34912f408bb3925bf21231cb6f59206267b7f63f3503ef865c1a292e25"}, ] pep440-version-utils = [ {file = "pep440-version-utils-0.3.0.tar.gz", hash = "sha256:ceb8c8da63b54cc555946d91829f72fe323f8d635b22fa54ef0a9800c37f50df"}, {file = "pep440_version_utils-0.3.0-py3-none-any.whl", hash = "sha256:73780b2c31adad5ca35c89eb008f51c2a47aee0318debe31391b673b90577e1b"}, ] pillow = [ - {file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"}, - {file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"}, - {file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"}, - {file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"}, - {file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"}, - {file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"}, - {file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"}, - {file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"}, - {file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"}, - {file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"}, - {file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"}, - {file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"}, - {file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"}, - {file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"}, - {file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"}, - {file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"}, - {file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"}, + {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, + {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, + {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, + {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, + {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, + {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, + {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, + {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, + {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, + {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, + {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, + {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, + {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, + {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -4621,32 +4627,32 @@ prompt-toolkit = [ {file = "prompt_toolkit-2.0.10.tar.gz", hash = "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"}, ] protobuf = [ - {file = "protobuf-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1cb2ed66aac593adbf6dca4f07cd7ee7e2958b17bbc85b2cc8bc564ebeb258ec"}, - {file = "protobuf-3.19.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:898bda9cd37ec0c781b598891e86435de80c3bfa53eb483a9dac5a11ec93e942"}, - {file = "protobuf-3.19.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad761ef3be34c8bdc7285bec4b40372a8dad9e70cfbdc1793cd3cf4c1a4ce74"}, - {file = "protobuf-3.19.3-cp310-cp310-win32.whl", hash = "sha256:2cddcbcc222f3144765ccccdb35d3621dc1544da57a9aca7e1944c1a4fe3db11"}, - {file = "protobuf-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:6202df8ee8457cb00810c6e76ced480f22a1e4e02c899a14e7b6e6e1de09f938"}, - {file = "protobuf-3.19.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:397d82f1c58b76445469c8c06b8dee1ff67b3053639d054f52599a458fac9bc6"}, - {file = "protobuf-3.19.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e54b8650e849ee8e95e481024bff92cf98f5ec61c7650cb838d928a140adcb63"}, - {file = "protobuf-3.19.3-cp36-cp36m-win32.whl", hash = "sha256:3bf3a07d17ba3511fe5fa916afb7351f482ab5dbab5afe71a7a384274a2cd550"}, - {file = "protobuf-3.19.3-cp36-cp36m-win_amd64.whl", hash = "sha256:afa8122de8064fd577f49ae9eef433561c8ace97a0a7b969d56e8b1d39b5d177"}, - {file = "protobuf-3.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18c40a1b8721026a85187640f1786d52407dc9c1ba8ec38accb57a46e84015f6"}, - {file = "protobuf-3.19.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:af7238849fa79285d448a24db686517570099739527a03c9c2971cce99cc5ae2"}, - {file = "protobuf-3.19.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e765e6dfbbb02c55e4d6d1145743401a84fc0b508f5a81b2c5a738cf86353139"}, - {file = "protobuf-3.19.3-cp37-cp37m-win32.whl", hash = "sha256:c781402ed5396ab56358d7b866d78c03a77cbc26ba0598d8bb0ac32084b1a257"}, - {file = "protobuf-3.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:544fe9705189b249380fae07952d220c97f5c6c9372a6f936cc83a79601dcb70"}, - {file = "protobuf-3.19.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84bf3aa3efb00dbe1c7ed55da0f20800b0662541e582d7e62b3e1464d61ed365"}, - {file = "protobuf-3.19.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3f80a3491eaca767cdd86cb8660dc778f634b44abdb0dffc9b2a8e8d0cd617d0"}, - {file = "protobuf-3.19.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9401d96552befcc7311f5ef8f0fa7dba0ef5fd805466b158b141606cd0ab6a8"}, - {file = "protobuf-3.19.3-cp38-cp38-win32.whl", hash = "sha256:ef02d112c025e83db5d1188a847e358beab3e4bbfbbaf10eaf69e67359af51b2"}, - {file = "protobuf-3.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:1291a0a7db7d792745c99d4657b4c5c4942695c8b1ac1bfb993a34035ec123f7"}, - {file = "protobuf-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49677e5e9c7ea1245a90c2e8a00d304598f22ea3aa0628f0e0a530a9e70665fa"}, - {file = "protobuf-3.19.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:df2ba379ee42427e8fcc6a0a76843bff6efb34ef5266b17f95043939b5e25b69"}, - {file = "protobuf-3.19.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2acd7ca329be544d1a603d5f13a4e34a3791c90d651ebaf130ba2e43ae5397c6"}, - {file = "protobuf-3.19.3-cp39-cp39-win32.whl", hash = "sha256:b53519b2ebec70cfe24b4ddda21e9843f0918d7c3627a785393fb35d402ab8ad"}, - {file = "protobuf-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:8ceaf5fdb72c8e1fcb7be9f2b3b07482ce058a3548180c0bdd5c7e4ac5e14165"}, - {file = "protobuf-3.19.3-py2.py3-none-any.whl", hash = "sha256:f6d4b5b7595a57e69eb7314c67bef4a3c745b4caf91accaf72913d8e0635111b"}, - {file = "protobuf-3.19.3.tar.gz", hash = "sha256:d975a6314fbf5c524d4981e24294739216b5fb81ef3c14b86fb4b045d6690907"}, + {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, + {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, + {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, + {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, + {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, + {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, + {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, + {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, + {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, + {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, + {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, + {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, + {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, + {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, + {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, + {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, + {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, + {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, ] psutil = [ {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"}, @@ -5040,8 +5046,8 @@ randomname = [ {file = "randomname-0.1.5.tar.gz", hash = "sha256:e10d14ea10895ee5bc417bdcc6d955e0b586f3bc67094ab87afcf8dcac23ab92"}, ] rasa-sdk = [ - {file = "rasa-sdk-3.0.4.tar.gz", hash = "sha256:36c5d7928e4f98b4966a11973a8b5798ecfa628619fcec01dcc8b56680bb9fa5"}, - {file = "rasa_sdk-3.0.4-py3-none-any.whl", hash = "sha256:0f0ff23ac7650d4ec98b6b26694babdfa7286da4c85d481ebd5aa44710e2f39a"}, + {file = "rasa-sdk-3.0.5.tar.gz", hash = "sha256:8b7862256e37af1cc478f7055d361501c12acc1ee9a65367c0b4c0ae18689610"}, + {file = "rasa_sdk-3.0.5-py3-none-any.whl", hash = "sha256:13530a1a2587badef39af4e56297e5ed1c78d604515b36a800305f0bc1019b05"}, ] redis = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, @@ -5095,9 +5101,8 @@ requests = [ {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] requests-oauthlib = [ - {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, - {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, - {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, ] requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, @@ -5124,6 +5129,10 @@ rsa = [ {file = "ruamel.yaml-0.16.13.tar.gz", hash = "sha256:bb48c514222702878759a05af96f4b7ecdba9b33cd4efcf25c86b882cef3a942"}, ] "ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, @@ -5147,35 +5156,31 @@ rsa = [ {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] s3transfer = [ - {file = "s3transfer-0.5.0-py3-none-any.whl", hash = "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803"}, - {file = "s3transfer-0.5.0.tar.gz", hash = "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c"}, + {file = "s3transfer-0.5.1-py3-none-any.whl", hash = "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f"}, + {file = "s3transfer-0.5.1.tar.gz", hash = "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a"}, ] sacremoses = [ {file = "sacremoses-0.0.47-py2.py3-none-any.whl", hash = "sha256:7622c6e9fe12d45b7acf4528451bd054c1557c1f6779398f9cd9f28332d92a0b"}, ] sanic = [ - {file = "sanic-21.9.3-py3-none-any.whl", hash = "sha256:354fbc9e5382df23989752067a57d042aef99bf5a8e85593a3e0112276bff9e8"}, - {file = "sanic-21.9.3.tar.gz", hash = "sha256:5edb41d0d30cf47a25cf991b7465f53ea161b43e3cd20d8985e68ecf7fb7ad24"}, + {file = "sanic-21.12.1-py3-none-any.whl", hash = "sha256:53230ba1a1081e6b075e58339a47c07d2bf618119fa40ba88c69fe57c99126f1"}, + {file = "sanic-21.12.1.tar.gz", hash = "sha256:6f15ecfef47d4288aac04d8741833a7701cd94f2d4a28fc5667ce2844363152d"}, ] sanic-cors = [ - {file = "Sanic-Cors-1.0.1.tar.gz", hash = "sha256:ac5d40bd45022d21296887d2238891d0ad67308ffba55be809df0151d36071b5"}, - {file = "Sanic_Cors-1.0.1-py2.py3-none-any.whl", hash = "sha256:dc644b0608ebac1bb57586a955c15c53a8bf83e2d0ea248893480c3cd72efdf6"}, + {file = "Sanic-Cors-2.0.1.tar.gz", hash = "sha256:4d2f26333d49db428217814c66e89fc3df20fc62a5ab518a71fa22e2e249e19d"}, + {file = "Sanic_Cors-2.0.1-py2.py3-none-any.whl", hash = "sha256:0c8132bed394ba86f93c03bef52787183652d96b70add4ea13c25eb98f344343"}, ] sanic-jwt = [ {file = "sanic-jwt-1.7.0.tar.gz", hash = "sha256:427a898338c77c314910c949c3ea9f397d093050453740492051df98b7c4cdbb"}, {file = "sanic_jwt-1.7.0-py3-none-any.whl", hash = "sha256:1dbcfec93cef77a41835aa38eae27780fc4255b9c1b067674ce2f0d2f5d45353"}, ] -sanic-plugin-toolkit = [ - {file = "sanic-plugin-toolkit-1.2.1.tar.gz", hash = "sha256:9f6eeca2e28b915dba4be8584ebb5a5f76a3f6895fe34572042ec275b13e41e1"}, - {file = "sanic_plugin_toolkit-1.2.1-py3-none-any.whl", hash = "sha256:aabea0dc1fd71969567c6a0fa419b55a727c0a5be372f166560e0cdbb9c30abb"}, -] sanic-routing = [ {file = "sanic-routing-0.7.2.tar.gz", hash = "sha256:139ce88b3f054e7aa336e2ecc8459837092b103b275d3a97609a34092c55374d"}, {file = "sanic_routing-0.7.2-py3-none-any.whl", hash = "sha256:523034ffd07aca056040e08de438269c9a880722eee1ace3a32e4f74b394d9aa"}, ] sanic-testing = [ - {file = "sanic-testing-0.7.0.tar.gz", hash = "sha256:8396507cdcc030f0b43a1b2419c04034be28bc6c466596e122e7bb407bb515b3"}, - {file = "sanic_testing-0.7.0-py3-none-any.whl", hash = "sha256:3e600981d830731c6108c9c2f12c577800c061c92787a1b1c9c8fa6a80960843"}, + {file = "sanic-testing-0.8.2.tar.gz", hash = "sha256:dd7123132e159281b14eb6434da811e2082165432aa2c523262e44b2c09c3be0"}, + {file = "sanic_testing-0.8.2-py3-none-any.whl", hash = "sha256:f2c3679cd498351f095d8687a1cc6cc10558fd69e014d060ec21f3a020d5723b"}, ] scikit-learn = [ {file = "scikit-learn-0.24.2.tar.gz", hash = "sha256:d14701a12417930392cd3898e9646cf5670c190b933625ebe7511b1f7d7b8736"}, @@ -5213,6 +5218,9 @@ scikit-learn = [ {file = "scikit_learn-0.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:40556bea1ef26ef54bc678d00cf138a63069144a0b5f3a436eecd8f3468b903e"}, ] scipy = [ + {file = "scipy-1.7.3-1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c9e04d7e9b03a8a6ac2045f7c5ef741be86727d8f49c45db45f244bdd2bcff17"}, + {file = "scipy-1.7.3-1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b0e0aeb061a1d7dcd2ed59ea57ee56c9b23dd60100825f98238c06ee5cc4467e"}, + {file = "scipy-1.7.3-1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:b78a35c5c74d336f42f44106174b9851c783184a85a3fe3e68857259b37b9ffb"}, {file = "scipy-1.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:173308efba2270dcd61cd45a30dfded6ec0085b4b6eb33b5eb11ab443005e088"}, {file = "scipy-1.7.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:21b66200cf44b1c3e86495e3a436fc7a26608f92b8d43d344457c54f1c024cbc"}, {file = "scipy-1.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceebc3c4f6a109777c0053dfa0282fddb8893eddfb0d598574acfb734a926168"}, @@ -5505,8 +5513,8 @@ thinc = [ {file = "thinc-8.0.13.tar.gz", hash = "sha256:47662a3ae33d445a77b6ea7b772444805c7bba8991f122e350daf72dedc8171a"}, ] threadpoolctl = [ - {file = "threadpoolctl-3.0.0-py3-none-any.whl", hash = "sha256:4fade5b3b48ae4b1c30f200b28f39180371104fccc642e039e0f2435ec8cc211"}, - {file = "threadpoolctl-3.0.0.tar.gz", hash = "sha256:d03115321233d0be715f0d3a5ad1d6c065fe425ddc2d671ca8e45e9fd5d7a52a"}, + {file = "threadpoolctl-3.1.0-py3-none-any.whl", hash = "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b"}, + {file = "threadpoolctl-3.1.0.tar.gz", hash = "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"}, ] tokenizers = [ {file = "tokenizers-0.7.0-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:c9edc043bc14462faf8b261b528661718e9c4f0b8424fb25be71cae26187432a"}, @@ -5540,8 +5548,8 @@ toolz = [ {file = "toolz-0.11.2.tar.gz", hash = "sha256:6b312d5e15138552f1bda8a4e66c30e236c831b612b2bf0005f8a1df10a4bc33"}, ] towncrier = [ - {file = "towncrier-21.3.0-py2.py3-none-any.whl", hash = "sha256:e6ccec65418bbcb8de5c908003e130e37fe0e9d6396cb77c1338241071edc082"}, - {file = "towncrier-21.3.0.tar.gz", hash = "sha256:6eed0bc924d72c98c000cb8a64de3bd566e5cb0d11032b73fcccf8a8f956ddfe"}, + {file = "towncrier-21.9.0-py2.py3-none-any.whl", hash = "sha256:fc5a88a2a54988e3a8ed2b60d553599da8330f65722cc607c839614ed87e0f92"}, + {file = "towncrier-21.9.0.tar.gz", hash = "sha256:9cb6f45c16e1a1eec9d0e7651165e7be60cd0ab81d13a5c96ca97a498ae87f48"}, ] tqdm = [ {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, @@ -5607,16 +5615,16 @@ types-pytz = [ {file = "types_pytz-2021.3.4-py3-none-any.whl", hash = "sha256:ccfa2ed29f816e3de2f882541c06ad2791f808a79cfe38265411820190999f0f"}, ] types-requests = [ - {file = "types-requests-2.27.7.tar.gz", hash = "sha256:f38bd488528cdcbce5b01dc953972f3cead0d060cfd9ee35b363066c25bab13c"}, - {file = "types_requests-2.27.7-py3-none-any.whl", hash = "sha256:2e0e100dd489f83870d4f61949d3a7eae4821e7bfbf46c57e463c38f92d473d4"}, + {file = "types-requests-2.27.9.tar.gz", hash = "sha256:7368974534d297939492efdfdab232930440b11e2203f6df1f0c40e3242a87ea"}, + {file = "types_requests-2.27.9-py3-none-any.whl", hash = "sha256:74070045418faf710f3154403d6a16c9e67db50e5119906ca6955f1658d20f7b"}, ] types-setuptools = [ - {file = "types-setuptools-57.4.7.tar.gz", hash = "sha256:9677d969b00ec1c14552f5be2b2b47a6fbea4d0ed4de0fdcee18abdaa0cc9267"}, - {file = "types_setuptools-57.4.7-py3-none-any.whl", hash = "sha256:ffda504687ea02d4b7751c0d1df517fbbcdc276836d90849e4f1a5f1ccd79f01"}, + {file = "types-setuptools-57.4.9.tar.gz", hash = "sha256:536ef74744f8e1e4be4fc719887f886e74e4cf3c792b4a06984320be4df450b5"}, + {file = "types_setuptools-57.4.9-py3-none-any.whl", hash = "sha256:948dc6863373750e2cd0b223a84f1fb608414cde5e55cf38ea657b93aeb411d2"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.7.tar.gz", hash = "sha256:cfd1fbbe4ba9a605ed148294008aac8a7b8b7472651d1cc357d507ae5962e3d2"}, - {file = "types_urllib3-1.26.7-py3-none-any.whl", hash = "sha256:3adcf2cb5981809091dbff456e6999fe55f201652d8c360f99997de5ac2f556e"}, + {file = "types-urllib3-1.26.9.tar.gz", hash = "sha256:abd2d4857837482b1834b4817f0587678dcc531dbc9abe4cde4da28cef3f522c"}, + {file = "types_urllib3-1.26.9-py3-none-any.whl", hash = "sha256:4a54f6274ab1c80968115634a55fb9341a699492b95e32104a7c513db9fe02e9"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, @@ -5778,8 +5786,8 @@ websockets = [ {file = "websockets-10.1.tar.gz", hash = "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"}, ] werkzeug = [ - {file = "Werkzeug-2.0.2-py3-none-any.whl", hash = "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f"}, - {file = "Werkzeug-2.0.2.tar.gz", hash = "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a"}, + {file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"}, + {file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, diff --git a/pyproject.toml b/pyproject.toml index 26280b4c6df5..fd84be032596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,11 +90,11 @@ colorhash = "~1.0.2" jsonschema = "~3.2" packaging = ">=20.0,<21.0" pytz = ">=2019.1,<2022.0" -rasa-sdk = "~3.0.4" +rasa-sdk = "~3.0.5" colorclass = "~2.2" terminaltables = "~3.1.0" -sanic = "^21.6.0" -sanic-cors = "^1.0.0" +sanic = "~21.12" +sanic-cors = "^2.0.0" sanic-jwt = "^1.6.0" cloudpickle = ">=1.2,<1.7" aiohttp = ">=3.6,<3.8,!=3.7.4.post0" @@ -163,7 +163,7 @@ types-setuptools = "^57.0.0" memory-profiler = "^0.58.0" psutil = "^5.8.0" mypy-extensions = "^0.4.3" -sanic-testing = "^0.7.0" +sanic-testing = "^0.8.0" [tool.poetry.extras] spacy = [ "spacy",] From 7fa4a0844f781ecb053a080764e3261cd42f53bb Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 09:51:56 +0100 Subject: [PATCH 02/65] fix deprecation errors - update all references to app.[user defined field] to app.ctx.[user defined field] - update all references to SocketBlueprint.[user defined field] to SocketBlueprint.ctx.user defined field] - add constant names for Sanic app names instead of using __name__ variable - replace sanic.exceptions.abort with sanic.exceptions.SanicException - replace sanic.response.StreamingHTTPResponse with sanic.response.ResponseStream --- rasa/core/channels/channel.py | 4 +- rasa/core/channels/hangouts.py | 6 +- rasa/core/channels/rasa_chat.py | 6 +- rasa/core/channels/socketio.py | 6 +- rasa/core/channels/twilio_voice.py | 2 +- rasa/core/run.py | 8 +-- rasa/core/utils.py | 2 +- rasa/server.py | 94 ++++++++++++++-------------- tests/core/channels/test_telegram.py | 2 +- tests/core/test_agent.py | 12 ++-- tests/core/test_channels.py | 18 +++--- tests/core/test_nlg.py | 2 +- tests/core/test_run.py | 2 +- tests/test_server.py | 38 ++++++----- 14 files changed, 103 insertions(+), 99 deletions(-) diff --git a/rasa/core/channels/channel.py b/rasa/core/channels/channel.py index d002fc69e5b1..5fca3ad9868e 100644 --- a/rasa/core/channels/channel.py +++ b/rasa/core/channels/channel.py @@ -86,7 +86,7 @@ def register( """Registers input channel blueprints with Sanic.""" async def handler(message: UserMessage) -> None: - await app.agent.handle_message(message) + await app.ctx.agent.handle_message(message) for channel in input_channels: if route: @@ -95,7 +95,7 @@ async def handler(message: UserMessage) -> None: p = None app.blueprint(channel.blueprint(handler), url_prefix=p) - app.input_channels = input_channels + app.ctx.input_channels = input_channels class InputChannel: diff --git a/rasa/core/channels/hangouts.py b/rasa/core/channels/hangouts.py index e1c4ffd24356..79c1f1f6fc71 100644 --- a/rasa/core/channels/hangouts.py +++ b/rasa/core/channels/hangouts.py @@ -10,7 +10,7 @@ from google.oauth2 import id_token from sanic.response import HTTPResponse -from sanic.exceptions import abort +from sanic.exceptions import SanicException from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage @@ -279,9 +279,9 @@ def _check_token(self, bot_token: Text) -> None: certs_url=CERTS_URL, ) except ValueError: - abort(401) + SanicException(status_code=401) if decoded_token["iss"] != "chat@system.gserviceaccount.com": - abort(401) + SanicException(status_code=401) def blueprint( self, on_new_message: Callable[[UserMessage], Awaitable[None]] diff --git a/rasa/core/channels/rasa_chat.py b/rasa/core/channels/rasa_chat.py index 1290ceb33ab7..a50afd6ee7fa 100644 --- a/rasa/core/channels/rasa_chat.py +++ b/rasa/core/channels/rasa_chat.py @@ -3,7 +3,7 @@ import aiohttp import logging -from sanic.exceptions import abort +from sanic.exceptions import SanicException import jwt import jwt.exceptions @@ -97,7 +97,7 @@ async def _extract_sender(self, req: Request) -> Optional[Text]: jwt_payload = await self._decode_bearer_token(req.args.get("token")) if not jwt_payload: - abort(401) + SanicException(status_code=401) if CONVERSATION_ID_KEY in req.json: if self._has_user_permission_to_send_messages_to_conversation( @@ -111,7 +111,7 @@ async def _extract_sender(self, req: Request) -> Optional[Text]: jwt_payload[JWT_USERNAME_KEY], req.json[CONVERSATION_ID_KEY] ) ) - abort(401) + SanicException(status_code=401) return jwt_payload[JWT_USERNAME_KEY] diff --git a/rasa/core/channels/socketio.py b/rasa/core/channels/socketio.py index bdae6e1aff9d..9d4c199c321b 100644 --- a/rasa/core/channels/socketio.py +++ b/rasa/core/channels/socketio.py @@ -17,12 +17,12 @@ class SocketBlueprint(Blueprint): def __init__( self, sio: AsyncServer, socketio_path: Text, *args: Any, **kwargs: Any ) -> None: - self.sio = sio - self.socketio_path = socketio_path super().__init__(*args, **kwargs) + self.ctx.sio = sio + self.ctx.socketio_path = socketio_path def register(self, app: Sanic, options: Dict[Text, Any]) -> None: - self.sio.attach(app, self.socketio_path) + self.ctx.sio.attach(app, self.ctx.socketio_path) super().register(app, options) diff --git a/rasa/core/channels/twilio_voice.py b/rasa/core/channels/twilio_voice.py index 7faae1ee5ed9..09130394edc9 100644 --- a/rasa/core/channels/twilio_voice.py +++ b/rasa/core/channels/twilio_voice.py @@ -246,7 +246,7 @@ async def receive(request: Request) -> HTTPResponse: # If the user doesn't respond resend the last message. else: # Get last user utterance from tracker. - tracker = request.app.agent.tracker_store.retrieve(sender_id) + tracker = request.app.ctx.agent.tracker_store.retrieve(sender_id) last_response = None if tracker: last_response = next( diff --git a/rasa/core/run.py b/rasa/core/run.py index 692683e1dc92..ba0bc01f6754 100644 --- a/rasa/core/run.py +++ b/rasa/core/run.py @@ -70,7 +70,7 @@ def _create_single_channel(channel: Text, credentials: Dict[Text, Any]) -> Any: def _create_app_without_api(cors: Optional[Union[Text, List[Text]]] = None) -> Sanic: - app = Sanic(__name__, configure_logging=False) + app = Sanic("rasa_core_no_api", configure_logging=False) server.add_root_route(app) server.configure_cors(app, cors) return app @@ -240,14 +240,14 @@ async def load_agent_on_start( Used to be scheduled on server start (hence the `app` and `loop` arguments). """ - app.agent = await agent.load_agent( + app.ctx.agent = await agent.load_agent( model_path=model_path, remote_storage=remote_storage, endpoints=endpoints, loop=loop, ) logger.info("Rasa server is up and running.") - return app.agent + return app.ctx.agent async def close_resources(app: Sanic, _: AbstractEventLoop) -> None: @@ -257,7 +257,7 @@ async def close_resources(app: Sanic, _: AbstractEventLoop) -> None: app: The Sanic application. _: The current Sanic worker event loop. """ - current_agent = getattr(app, "agent", None) + current_agent = getattr(app.ctx, "agent", None) if not current_agent: logger.debug("No agent found when shutting down server.") return diff --git a/rasa/core/utils.py b/rasa/core/utils.py index e1e9235ce194..68d57165d7fe 100644 --- a/rasa/core/utils.py +++ b/rasa/core/utils.py @@ -117,7 +117,7 @@ def find_route(suffix: Text, path: Text) -> Optional[Text]: for arg in route._params: options[arg] = f"[{arg}]" - handlers = [(list(route.methods)[0], route.name.replace("rasa.server.", ""))] + handlers = [(list(route.methods)[0], route.name.replace("rasa_server.", ""))] for method, name in handlers: full_endpoint = "/" + "/".join(endpoint) diff --git a/rasa/server.py b/rasa/server.py index 815d508ff954..203ec5b35ca7 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -146,7 +146,7 @@ def decorator(f: Callable) -> Callable: @wraps(f) def decorated(*args: Any, **kwargs: Any) -> Any: # noinspection PyUnresolvedReferences - if not app.agent or not app.agent.is_ready(): + if not app.ctx.agent or not app.ctx.agent.is_ready(): raise ErrorResponse( HTTPStatus.CONFLICT, "Conflict", @@ -169,7 +169,7 @@ def decorator(f: "SanicView") -> "SanicView": @wraps(f) def decorated(request: Request, *args: Any, **kwargs: Any) -> "SanicResponse": conversation_id = kwargs["conversation_id"] - if request.app.agent.tracker_store.exists(conversation_id): + if request.app.ctx.agent.tracker_store.exists(conversation_id): return f(request, *args, **kwargs) else: raise ErrorResponse( @@ -638,7 +638,7 @@ def create_app( endpoints: Optional[AvailableEndpoints] = None, ) -> Sanic: """Class representing a Rasa HTTP server.""" - app = Sanic(__name__) + app = Sanic("rasa_server") app.config.RESPONSE_TIMEOUT = response_timeout configure_cors(app, cors_origins) @@ -665,10 +665,10 @@ def create_app( user_id="username", ) - app.agent = agent + app.ctx.agent = agent # Initialize shared object of type unsigned int for tracking # the number of active training processes - app.active_training_processes = multiprocessing.Value("I", 0) + app.ctx.active_training_processes = multiprocessing.Value("I", 0) @app.exception(ErrorResponse) async def handle_error_response( @@ -696,9 +696,9 @@ async def status(request: Request) -> HTTPResponse: """Respond with the model name and the fingerprint of that model.""" return response.json( { - "model_file": app.agent.processor.model_filename, - "model_id": app.agent.model_id, - "num_active_training_jobs": app.active_training_processes.value, + "model_file": app.ctx.agent.processor.model_filename, + "model_id": app.ctx.agent.model_id, + "num_active_training_jobs": app.ctx.active_training_processes.value, } ) @@ -710,7 +710,7 @@ async def retrieve_tracker(request: Request, conversation_id: Text) -> HTTPRespo verbosity = event_verbosity_parameter(request, EventVerbosity.AFTER_RESTART) until_time = rasa.utils.endpoints.float_arg(request, "until") - tracker = await app.agent.processor.fetch_tracker_with_initial_session( + tracker = await app.ctx.agent.processor.fetch_tracker_with_initial_session( conversation_id ) @@ -738,12 +738,12 @@ async def append_events(request: Request, conversation_id: Text) -> HTTPResponse verbosity = event_verbosity_parameter(request, EventVerbosity.AFTER_RESTART) try: - async with app.agent.lock_store.lock(conversation_id): - processor = app.agent.processor + async with app.ctx.agent.lock_store.lock(conversation_id): + processor = app.ctx.agent.processor events = _get_events_from_request_body(request) tracker = await update_conversation_with_events( - conversation_id, processor, app.agent.domain, events + conversation_id, processor, app.ctx.agent.domain, events ) output_channel = _get_output_channel(request, tracker) @@ -755,7 +755,7 @@ async def append_events(request: Request, conversation_id: Text) -> HTTPResponse events, tracker, output_channel ) - app.agent.tracker_store.save(tracker) + app.ctx.agent.tracker_store.save(tracker) return response.json(tracker.current_state(verbosity)) except Exception as e: @@ -799,13 +799,13 @@ async def replace_events(request: Request, conversation_id: Text) -> HTTPRespons verbosity = event_verbosity_parameter(request, EventVerbosity.AFTER_RESTART) try: - async with app.agent.lock_store.lock(conversation_id): + async with app.ctx.agent.lock_store.lock(conversation_id): tracker = DialogueStateTracker.from_dict( - conversation_id, request.json, app.agent.domain.slots + conversation_id, request.json, app.ctx.agent.domain.slots ) # will override an existing tracker with the same id! - app.agent.tracker_store.save(tracker) + app.ctx.agent.tracker_store.save(tracker) return response.json(tracker.current_state(verbosity)) except Exception as e: @@ -829,7 +829,7 @@ async def retrieve_story(request: Request, conversation_id: Text) -> HTTPRespons try: stories = get_test_stories( - app.agent.processor, + app.ctx.agent.processor, conversation_id, until_time, fetch_all_sessions=fetch_all_sessions, @@ -865,15 +865,15 @@ async def execute_action(request: Request, conversation_id: Text) -> HTTPRespons verbosity = event_verbosity_parameter(request, EventVerbosity.AFTER_RESTART) try: - async with app.agent.lock_store.lock(conversation_id): + async with app.ctx.agent.lock_store.lock(conversation_id): tracker = await ( - app.agent.processor.fetch_tracker_and_update_session( + app.ctx.agent.processor.fetch_tracker_and_update_session( conversation_id ) ) output_channel = _get_output_channel(request, tracker) - await app.agent.execute_action( + await app.ctx.agent.execute_action( conversation_id, action_to_execute, output_channel, @@ -918,20 +918,20 @@ async def trigger_intent(request: Request, conversation_id: Text) -> HTTPRespons verbosity = event_verbosity_parameter(request, EventVerbosity.AFTER_RESTART) try: - async with app.agent.lock_store.lock(conversation_id): + async with app.ctx.agent.lock_store.lock(conversation_id): tracker = await ( - app.agent.processor.fetch_tracker_and_update_session( + app.ctx.agent.processor.fetch_tracker_and_update_session( conversation_id ) ) output_channel = _get_output_channel(request, tracker) - if intent_to_trigger not in app.agent.domain.intents: + if intent_to_trigger not in app.ctx.agent.domain.intents: raise ErrorResponse( HTTPStatus.NOT_FOUND, "NotFound", f"The intent {trigger_intent} does not exist in the domain.", ) - await app.agent.trigger_intent( + await app.ctx.agent.trigger_intent( intent_name=intent_to_trigger, entities=entities, output_channel=output_channel, @@ -963,7 +963,7 @@ async def trigger_intent(request: Request, conversation_id: Text) -> HTTPRespons async def predict(request: Request, conversation_id: Text) -> HTTPResponse: try: # Fetches the appropriate bot response in a json format - responses = await app.agent.predict_next_for_sender_id(conversation_id) + responses = await app.ctx.agent.predict_next_for_sender_id(conversation_id) responses["scores"] = sorted( responses["scores"], key=lambda k: (-k["score"], k["action"]) ) @@ -1007,15 +1007,15 @@ async def add_message(request: Request, conversation_id: Text) -> HTTPResponse: user_message = UserMessage(message, None, conversation_id, parse_data) try: - async with app.agent.lock_store.lock(conversation_id): + async with app.ctx.agent.lock_store.lock(conversation_id): # cf. processor.handle_message (ignoring prediction loop run) - tracker = await app.agent.processor.log_message( + tracker = await app.ctx.agent.processor.log_message( user_message, should_save_tracker=False ) - tracker = await app.agent.processor.run_action_extract_slots( + tracker = await app.ctx.agent.processor.run_action_extract_slots( user_message.output_channel, tracker ) - app.agent.processor.save_tracker(tracker) + app.ctx.agent.processor.save_tracker(tracker) return response.json(tracker.current_state(verbosity)) except Exception as e: @@ -1041,8 +1041,8 @@ async def train(request: Request, temporary_directory: Path) -> HTTPResponse: training_payload = _training_payload_from_yaml(request, temporary_directory) try: - with app.active_training_processes.get_lock(): - app.active_training_processes.value += 1 + with app.ctx.active_training_processes.get_lock(): + app.ctx.active_training_processes.value += 1 from rasa.model_training import train @@ -1079,8 +1079,8 @@ async def train(request: Request, temporary_directory: Path) -> HTTPResponse: f"An unexpected error occurred during training. Error: {e}", ) finally: - with app.active_training_processes.get_lock(): - app.active_training_processes.value -= 1 + with app.ctx.active_training_processes.get_lock(): + app.ctx.active_training_processes.value -= 1 @app.post("/model/test/stories") @requires_auth(app, auth_token) @@ -1102,7 +1102,7 @@ async def evaluate_stories( try: evaluation = await test( - test_data, app.agent, e2e=e2e, disable_plotting=True + test_data, app.ctx.agent, e2e=e2e, disable_plotting=True ) return response.json(evaluation) except Exception as e: @@ -1171,10 +1171,10 @@ async def _evaluate_model_using_test_set( ) -> Dict: logger.info("Starting model evaluation using test set.") - eval_agent = app.agent + eval_agent = app.ctx.agent if model_path: - model_server = app.agent.model_server + model_server = app.ctx.agent.model_server if model_server is not None: model_server = model_server.copy() model_server.url = model_path @@ -1184,7 +1184,7 @@ async def _evaluate_model_using_test_set( eval_agent = await _load_agent( model_path=model_path, model_server=model_server, - remote_storage=app.agent.remote_storage, + remote_storage=app.ctx.agent.remote_storage, ) data_path = os.path.abspath(test_data_file) @@ -1251,7 +1251,7 @@ async def tracker_predict(request: Request) -> HTTPResponse: request_params = request.json try: tracker = DialogueStateTracker.from_dict( - DEFAULT_SENDER_ID, request_params, app.agent.domain.slots + DEFAULT_SENDER_ID, request_params, app.ctx.agent.domain.slots ) except Exception as e: logger.debug(traceback.format_exc()) @@ -1263,7 +1263,7 @@ async def tracker_predict(request: Request) -> HTTPResponse: ) try: - result = app.agent.predict_next_with_tracker(tracker, verbosity) + result = app.ctx.agent.predict_next_with_tracker(tracker, verbosity) return response.json(result) except Exception as e: @@ -1289,7 +1289,7 @@ async def parse(request: Request) -> HTTPResponse: try: data = emulator.normalise_request_json(request.json) try: - parsed_data = await app.agent.parse_message(data.get("text")) + parsed_data = await app.ctx.agent.parse_message(data.get("text")) except Exception as e: logger.debug(traceback.format_exc()) raise ErrorResponse( @@ -1336,8 +1336,8 @@ async def load_model(request: Request) -> HTTPResponse: remote_storage=remote_storage, endpoints=endpoints, ) - new_agent.lock_store = app.agent.lock_store - app.agent = new_agent + new_agent.lock_store = app.ctx.agent.lock_store + app.ctx.agent = new_agent logger.debug(f"Successfully loaded model '{model_path}'.") return response.json(None, status=HTTPStatus.NO_CONTENT) @@ -1345,9 +1345,9 @@ async def load_model(request: Request) -> HTTPResponse: @app.delete("/model") @requires_auth(app, auth_token) async def unload_model(request: Request) -> HTTPResponse: - model_file = app.agent.model_name + model_file = app.ctx.agent.model_name - app.agent = Agent(lock_store=app.agent.lock_store) + app.ctx.agent = Agent(lock_store=app.ctx.agent.lock_store) logger.debug(f"Successfully unloaded model '{model_file}'.") return response.json(None, status=HTTPStatus.NO_CONTENT) @@ -1359,10 +1359,10 @@ async def get_domain(request: Request) -> HTTPResponse: """Get current domain in yaml or json format.""" accepts = request.headers.get("Accept", default=JSON_CONTENT_TYPE) if accepts.endswith("json"): - domain = app.agent.domain.as_dict() + domain = app.ctx.agent.domain.as_dict() return response.json(domain) elif accepts.endswith("yml") or accepts.endswith("yaml"): - domain_yaml = app.agent.domain.as_yaml() + domain_yaml = app.ctx.agent.domain.as_yaml() return response.text( domain_yaml, status=HTTPStatus.OK, content_type=YAML_CONTENT_TYPE ) @@ -1403,7 +1403,7 @@ def _get_output_channel( requested_output_channel = tracker.get_latest_input_channel() # Interactive training does not set `input_channels`, hence we have to be cautious - registered_input_channels = getattr(request.app, "input_channels", None) or [] + registered_input_channels = getattr(request.app.ctx, "input_channels", None) or [] matching_channels = [ channel for channel in registered_input_channels diff --git a/tests/core/channels/test_telegram.py b/tests/core/channels/test_telegram.py index 32f8107095c5..d7589733a480 100644 --- a/tests/core/channels/test_telegram.py +++ b/tests/core/channels/test_telegram.py @@ -56,7 +56,7 @@ def test_telegram_edit_message(): ) app = rasa.core.run.configure_app([input_channel], port=5004) - app.agent = Agent() + app.ctx.agent = Agent() _, res = app.test_client.post( "/webhooks/telegram/webhook", json=json.dumps(telegram_test_edited_message) ) diff --git a/tests/core/test_agent.py b/tests/core/test_agent.py index 724057367ccd..10b886bc07d0 100644 --- a/tests/core/test_agent.py +++ b/tests/core/test_agent.py @@ -12,7 +12,7 @@ from pytest_sanic.utils import TestClient from sanic import Sanic, response from sanic.request import Request -from sanic.response import StreamingHTTPResponse +from sanic.response import ResponseStream import rasa.core from rasa.core.exceptions import AgentNotReady @@ -39,17 +39,17 @@ def model_server_app(model_path: Text, model_hash: Text = "somehash") -> Sanic: - app = Sanic(__name__) - app.number_of_model_requests = 0 + app = Sanic("test_agent") + app.ctx.number_of_model_requests = 0 @app.route("/model", methods=["GET"]) - async def model(request: Request) -> StreamingHTTPResponse: + async def model(request: Request) -> ResponseStream: """Simple HTTP model server responding with a trained model.""" if model_hash == request.headers.get("If-None-Match"): return response.text("", 204) - app.number_of_model_requests += 1 + app.ctx.number_of_model_requests += 1 return await response.file_stream( location=model_path, @@ -156,7 +156,7 @@ async def test_agent_with_model_server_in_thread( assert agent.domain.as_dict() == domain.as_dict() assert agent.processor.graph_runner - assert model_server.app.number_of_model_requests == 1 + assert model_server.app.ctx.number_of_model_requests == 1 jobs.kill_scheduler() diff --git a/tests/core/test_channels.py b/tests/core/test_channels.py index c9ba5cf8c089..9dc380a83e11 100644 --- a/tests/core/test_channels.py +++ b/tests/core/test_channels.py @@ -569,13 +569,13 @@ def test_register_channel_without_route(): input_channel = RestInput() - app = Sanic(__name__) + app = Sanic("test_channels") rasa.core.channels.channel.register([input_channel], app, route=None) routes_list = utils.list_routes(app) - assert routes_list[ - "tests.core.test_channels.custom_webhook_RestInput.receive" - ].startswith("/webhook") + assert routes_list["test_channels.custom_webhook_RestInput.receive"].startswith( + "/webhook" + ) def test_channel_registration_with_absolute_url_prefix_overwrites_route(): @@ -586,7 +586,7 @@ def test_channel_registration_with_absolute_url_prefix_overwrites_route(): test_route = "/absolute_route" input_channel.url_prefix = lambda: test_route - app = Sanic(__name__) + app = Sanic("test_channels") ignored_base_route = "/should_be_ignored" rasa.core.channels.channel.register( [input_channel], app, route="/should_be_ignored" @@ -595,11 +595,11 @@ def test_channel_registration_with_absolute_url_prefix_overwrites_route(): # Assure that an absolute url returned by `url_prefix` overwrites route parameter # given in `register`. routes_list = utils.list_routes(app) - assert routes_list[ - "tests.core.test_channels.custom_webhook_RestInput.health" - ].startswith(test_route) + assert routes_list["test_channels.custom_webhook_RestInput.health"].startswith( + test_route + ) assert ignored_base_route not in routes_list.get( - "tests.core.test_channels.custom_webhook_RestInput.health" + "test_channels.custom_webhook_RestInput.health" ) diff --git a/tests/core/test_nlg.py b/tests/core/test_nlg.py index 1244f01d2243..35bdf262ca90 100644 --- a/tests/core/test_nlg.py +++ b/tests/core/test_nlg.py @@ -13,7 +13,7 @@ def nlg_app(base_url="/"): - app = Sanic(__name__) + app = Sanic("test_nlg") @app.route(base_url, methods=["POST"]) async def generate(request): diff --git a/tests/core/test_run.py b/tests/core/test_run.py index 1510035e8017..d57f1263c74c 100644 --- a/tests/core/test_run.py +++ b/tests/core/test_run.py @@ -81,7 +81,7 @@ async def test_load_agent_on_start_with_bad_model_file( async def test_close_resources(loop: AbstractEventLoop): broker = SQLEventBroker() app = Mock() - app.agent.tracker_store.event_broker = broker + app.ctx.agent.tracker_store.event_broker = broker with pytest.warns(None) as warnings: await run.close_resources(app, loop) diff --git a/tests/test_server.py b/tests/test_server.py index 90062e12cc2d..2d144129d5c3 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -5,6 +5,7 @@ import urllib.parse import uuid import sys +from argparse import Namespace from http import HTTPStatus from multiprocessing import Process, Manager from multiprocessing.managers import DictProxy @@ -199,7 +200,7 @@ async def test_status_secured(rasa_secured_app: SanicASGITestClient): async def test_status_not_ready_agent(rasa_app: SanicASGITestClient): - rasa_app.sanic_app.agent = None + rasa_app.sanic_app.ctx.agent = None _, response = await rasa_app.get("/status") assert response.status == HTTPStatus.CONFLICT @@ -938,7 +939,7 @@ async def test_evaluate_intent_with_model_server( agent_with_model_server = await load_agent( model_server=EndpointConfig(production_model_server_url) ) - rasa_app.sanic_app.agent = agent_with_model_server + rasa_app.sanic_app.ctx.agent = agent_with_model_server _, response = await rasa_app.post( f"/model/test/intents?model={test_model_server_url}", @@ -953,7 +954,7 @@ async def test_evaluate_intent_with_model_server( "response_selection_evaluation", } - production_model_server = rasa_app.sanic_app.agent.model_server + production_model_server = rasa_app.sanic_app.ctx.agent.model_server # Assert that the model server URL for the test didn't override the production # model server URL assert production_model_server.url == production_model_server_url @@ -1203,7 +1204,7 @@ async def test_replace_events_empty_request_body(rasa_app: SanicASGITestClient): @freeze_time("2018-01-01") async def test_requesting_non_existent_tracker(rasa_app: SanicASGITestClient): - model_id = rasa_app.sanic_app.agent.model_id + model_id = rasa_app.sanic_app.ctx.agent.model_id _, response = await rasa_app.get("/conversations/madeupid/tracker") content = response.json assert response.status == HTTPStatus.OK @@ -1252,7 +1253,7 @@ async def test_requesting_non_existent_tracker(rasa_app: SanicASGITestClient): @pytest.mark.parametrize("event", test_events) async def test_pushing_event(rasa_app: SanicASGITestClient, event: Event): - model_id = rasa_app.sanic_app.agent.model_id + model_id = rasa_app.sanic_app.ctx.agent.model_id sender_id = str(uuid.uuid1()) conversation = f"/conversations/{sender_id}" @@ -1289,7 +1290,7 @@ async def test_pushing_event(rasa_app: SanicASGITestClient, event: Event): async def test_pushing_event_with_existing_model_id(rasa_app: SanicASGITestClient): - model_id = rasa_app.sanic_app.agent.model_id + model_id = rasa_app.sanic_app.ctx.agent.model_id sender_id = str(uuid.uuid1()) conversation = f"/conversations/{sender_id}" @@ -1316,7 +1317,7 @@ async def test_pushing_event_with_existing_model_id(rasa_app: SanicASGITestClien async def test_push_multiple_events(rasa_app: SanicASGITestClient): - model_id = rasa_app.sanic_app.agent.model_id + model_id = rasa_app.sanic_app.ctx.agent.model_id conversation_id = str(uuid.uuid1()) conversation = f"/conversations/{conversation_id}" @@ -1378,7 +1379,7 @@ async def test_pushing_event_while_executing_side_effects( async def test_post_conversation_id_with_slash(rasa_app: SanicASGITestClient): - model_id = rasa_app.sanic_app.agent.model_id + model_id = rasa_app.sanic_app.ctx.agent.model_id conversation_id = str(uuid.uuid1()) id_len = len(conversation_id) // 2 conversation_id = conversation_id[:id_len] + "/+-_\\=" + conversation_id[id_len:] @@ -1749,7 +1750,8 @@ def test_get_output_channel( ): request = MagicMock() app = MagicMock() - app.input_channels = input_channels + app.ctx = Namespace() + app.ctx.input_channels = input_channels request.app = app request.args = {"output_channel": output_channel_to_use} @@ -1769,7 +1771,8 @@ def test_get_output_channel( def test_get_latest_output_channel(input_channels: List[Text], expected_channel: Type): request = MagicMock() app = MagicMock() - app.input_channels = input_channels + app.ctx = Namespace() + app.ctx.input_channels = input_channels request.app = app request.args = {"output_channel": "latest"} @@ -1786,6 +1789,7 @@ def test_app_when_app_has_no_input_channels(): request = MagicMock() class NoInputChannels: + ctx = Namespace() pass request.app = NoInputChannels() @@ -1959,9 +1963,9 @@ async def test_get_story( tracker_store.save(tracker) - monkeypatch.setattr(rasa_app.sanic_app.agent, "tracker_store", tracker_store) + monkeypatch.setattr(rasa_app.sanic_app.ctx.agent, "tracker_store", tracker_store) monkeypatch.setattr( - rasa_app.sanic_app.agent.processor, "tracker_store", tracker_store + rasa_app.sanic_app.ctx.agent.processor, "tracker_store", tracker_store ) url = f"/conversations/{conversation_id}/story?" @@ -2003,7 +2007,7 @@ async def test_get_story_does_not_update_conversation_session( session_expiration_time=1 / 60, carry_over_slots=True ) - monkeypatch.setattr(rasa_app.sanic_app.agent.processor, "domain", domain) + monkeypatch.setattr(rasa_app.sanic_app.ctx.agent.processor, "domain", domain) # conversation contains one session that has expired now = time.time() @@ -2017,15 +2021,15 @@ async def test_get_story_does_not_update_conversation_session( tracker = DialogueStateTracker.from_events(conversation_id, conversation_events) # the conversation session has expired - assert rasa_app.sanic_app.agent.processor._has_session_expired(tracker) + assert rasa_app.sanic_app.ctx.agent.processor._has_session_expired(tracker) tracker_store = InMemoryTrackerStore(domain) tracker_store.save(tracker) - monkeypatch.setattr(rasa_app.sanic_app.agent, "tracker_store", tracker_store) + monkeypatch.setattr(rasa_app.sanic_app.ctx.agent, "tracker_store", tracker_store) monkeypatch.setattr( - rasa_app.sanic_app.agent.processor, "tracker_store", tracker_store + rasa_app.sanic_app.ctx.agent.processor, "tracker_store", tracker_store ) _, response = await rasa_app.get(f"/conversations/{conversation_id}/story") @@ -2108,7 +2112,7 @@ async def test_update_conversation_with_events( expected_events: List[Event], ): conversation_id = "some-conversation-ID" - agent = rasa_app.sanic_app.agent + agent = rasa_app.sanic_app.ctx.agent tracker_store = agent.tracker_store domain = agent.domain model_id = agent.model_id From 6b89c306b0d9614988bac6db22876fe94af78b1c Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 11:15:03 +0100 Subject: [PATCH 03/65] add missing docstrings --- rasa/core/channels/socketio.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rasa/core/channels/socketio.py b/rasa/core/channels/socketio.py index 9d4c199c321b..3753a8fac897 100644 --- a/rasa/core/channels/socketio.py +++ b/rasa/core/channels/socketio.py @@ -17,11 +17,22 @@ class SocketBlueprint(Blueprint): def __init__( self, sio: AsyncServer, socketio_path: Text, *args: Any, **kwargs: Any ) -> None: + """Creates a :class:`sanic.Blueprint` for routing socketio connenctions. + + :param sio: Instance of :class:`socketio.AsyncServer` class + :param socketio_path: string indicating the route to accept requests on. + """ super().__init__(*args, **kwargs) self.ctx.sio = sio self.ctx.socketio_path = socketio_path def register(self, app: Sanic, options: Dict[Text, Any]) -> None: + """Attach the Socket.IO webserver to the given Sanic instance. + + :param app: Instance of :class:`sanic.app.Sanic` class + :param options: Options to be used while registering the + blueprint into the app. + """ self.ctx.sio.attach(app, self.ctx.socketio_path) super().register(app, options) From a79def7d5fd2a853412d2f222fa199d7dc4cc4f0 Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 11:31:53 +0100 Subject: [PATCH 04/65] fix type issues raised by mypy --- rasa/core/channels/hangouts.py | 4 ++-- rasa/core/channels/rasa_chat.py | 4 ++-- rasa/core/channels/rest.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rasa/core/channels/hangouts.py b/rasa/core/channels/hangouts.py index 79c1f1f6fc71..1a7c19a96867 100644 --- a/rasa/core/channels/hangouts.py +++ b/rasa/core/channels/hangouts.py @@ -279,9 +279,9 @@ def _check_token(self, bot_token: Text) -> None: certs_url=CERTS_URL, ) except ValueError: - SanicException(status_code=401) + raise SanicException(status_code=401) if decoded_token["iss"] != "chat@system.gserviceaccount.com": - SanicException(status_code=401) + raise SanicException(status_code=401) def blueprint( self, on_new_message: Callable[[UserMessage], Awaitable[None]] diff --git a/rasa/core/channels/rasa_chat.py b/rasa/core/channels/rasa_chat.py index a50afd6ee7fa..b6037a533175 100644 --- a/rasa/core/channels/rasa_chat.py +++ b/rasa/core/channels/rasa_chat.py @@ -97,7 +97,7 @@ async def _extract_sender(self, req: Request) -> Optional[Text]: jwt_payload = await self._decode_bearer_token(req.args.get("token")) if not jwt_payload: - SanicException(status_code=401) + raise SanicException(status_code=401) if CONVERSATION_ID_KEY in req.json: if self._has_user_permission_to_send_messages_to_conversation( @@ -111,7 +111,7 @@ async def _extract_sender(self, req: Request) -> Optional[Text]: jwt_payload[JWT_USERNAME_KEY], req.json[CONVERSATION_ID_KEY] ) ) - SanicException(status_code=401) + raise SanicException(status_code=401) return jwt_payload[JWT_USERNAME_KEY] diff --git a/rasa/core/channels/rest.py b/rasa/core/channels/rest.py index 761127c3d883..5f6fb5c6b935 100644 --- a/rasa/core/channels/rest.py +++ b/rasa/core/channels/rest.py @@ -5,7 +5,7 @@ from asyncio import Queue, CancelledError from sanic import Blueprint, response from sanic.request import Request -from sanic.response import HTTPResponse +from sanic.response import HTTPResponse, ResponseStream from typing import Text, Dict, Any, Optional, Callable, Awaitable, NoReturn import rasa.utils.endpoints @@ -97,7 +97,7 @@ async def health(request: Request) -> HTTPResponse: return response.json({"status": "ok"}) @custom_webhook.route("/webhook", methods=["POST"]) - async def receive(request: Request) -> HTTPResponse: + async def receive(request: Request) -> ResponseStream | HTTPResponse: sender_id = await self._extract_sender(request) text = self._extract_message(request) should_use_stream = rasa.utils.endpoints.bool_arg( From 478369eb107ba2fe0e376723367af44e040ff3c1 Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 12:24:31 +0100 Subject: [PATCH 05/65] correctly specify union type --- rasa/core/channels/rest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rasa/core/channels/rest.py b/rasa/core/channels/rest.py index 5f6fb5c6b935..f70c5edb71a5 100644 --- a/rasa/core/channels/rest.py +++ b/rasa/core/channels/rest.py @@ -6,7 +6,7 @@ from sanic import Blueprint, response from sanic.request import Request from sanic.response import HTTPResponse, ResponseStream -from typing import Text, Dict, Any, Optional, Callable, Awaitable, NoReturn +from typing import Text, Dict, Any, Optional, Callable, Awaitable, NoReturn, Union import rasa.utils.endpoints from rasa.core.channels.channel import ( @@ -97,7 +97,7 @@ async def health(request: Request) -> HTTPResponse: return response.json({"status": "ok"}) @custom_webhook.route("/webhook", methods=["POST"]) - async def receive(request: Request) -> ResponseStream | HTTPResponse: + async def receive(request: Request) -> Union[ResponseStream, HTTPResponse]: sender_id = await self._extract_sender(request) text = self._extract_message(request) should_use_stream = rasa.utils.endpoints.bool_arg( From 70db7709ae2bf84aa8fc6614699e43cadd1188f3 Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 15:43:14 +0100 Subject: [PATCH 06/65] update changelgo --- changelog/10412.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/10412.bugfix.md diff --git a/changelog/10412.bugfix.md b/changelog/10412.bugfix.md new file mode 100644 index 000000000000..2bbbad1c169f --- /dev/null +++ b/changelog/10412.bugfix.md @@ -0,0 +1 @@ +Fix Socket IO connection issues by upgrading sanic to v21.12 \ No newline at end of file From 22f470dfbdd15a9da783312b13f1ca1c54e4d72a Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 16:27:22 +0100 Subject: [PATCH 07/65] add context to changelog --- changelog/10412.bugfix.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/changelog/10412.bugfix.md b/changelog/10412.bugfix.md index 2bbbad1c169f..2dac79c043d8 100644 --- a/changelog/10412.bugfix.md +++ b/changelog/10412.bugfix.md @@ -1 +1,14 @@ -Fix Socket IO connection issues by upgrading sanic to v21.12 \ No newline at end of file +Fix Socket IO connection issues by upgrading sanic to v21.12. + +The bug is caused by [an invalid function signature](https://github.com/sanic-org/sanic/issues/2272) and is fixed in [v21.12](https://sanic.readthedocs.io/en/v21.12.1/sanic/changelog.html#version-21-12-0). + +This update brings some deprecations in `sanic`: + +- Sanic and Blueprint may no longer have arbitrary properties attached to them + - Fixed this by moving user defined properties to the `instance.ctx` object +- Sanic and Blueprint forced to have compliant names + - Fixed this by using string literal names instead of the module's name via _\_name\_\_ +- `sanic.exceptions.abort` is Deprecated + - Fixed by replacing it with `sanic.exceptions.SanicException` +- `sanic.response.StreamingHTTPResponse` is deprecated + - Fixed by replacing it with sanic.response.ResponseStream From c318259dee1d79ced81bfe7ece774f18618b4d98 Mon Sep 17 00:00:00 2001 From: Daniel Onodje Date: Fri, 11 Feb 2022 16:33:03 +0100 Subject: [PATCH 08/65] add ctx property when creating MagicMock --- tests/test_server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 2d144129d5c3..f4e658096d96 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1749,8 +1749,7 @@ def test_get_output_channel( input_channels: List[Text], output_channel_to_use: Text, expected_channel: Type ): request = MagicMock() - app = MagicMock() - app.ctx = Namespace() + app = MagicMock(ctx=Namespace()) app.ctx.input_channels = input_channels request.app = app request.args = {"output_channel": output_channel_to_use} @@ -1770,8 +1769,7 @@ def test_get_output_channel( ) def test_get_latest_output_channel(input_channels: List[Text], expected_channel: Type): request = MagicMock() - app = MagicMock() - app.ctx = Namespace() + app = MagicMock(ctx=Namespace()) app.ctx.input_channels = input_channels request.app = app request.args = {"output_channel": "latest"} From 42b0410b96cc28a1b3b27495868815a49b8e8ff9 Mon Sep 17 00:00:00 2001 From: Sarah Elsharkawy <93398547+s-elsharkawy@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:29:54 +0100 Subject: [PATCH 09/65] Update_confidence_values_in_docs (#10891) Rephrased the paragraph about the allowed confidence values for TEDPolicy and DIETClassifier to reflect the actual allowed values. Therefore, removed the linear_norm because it is no longer supported. --- changelog/10798.doc.md | 2 ++ docs/docs/components.mdx | 19 +++++++------------ 2 files changed, 9 insertions(+), 12 deletions(-) create mode 100644 changelog/10798.doc.md diff --git a/changelog/10798.doc.md b/changelog/10798.doc.md new file mode 100644 index 000000000000..8fe93086e740 --- /dev/null +++ b/changelog/10798.doc.md @@ -0,0 +1,2 @@ +Updated the `model_confidence` parameter in `TEDPolicy` and `DIETClassifier`. The `linear_norm` is removed +as it is no longer supported. \ No newline at end of file diff --git a/docs/docs/components.mdx b/docs/docs/components.mdx index c00054ffd96d..1b524b0ec3c9 100644 --- a/docs/docs/components.mdx +++ b/docs/docs/components.mdx @@ -1316,12 +1316,10 @@ Intent classifiers assign one of the intents defined in the domain file to incom This should help in better generalization of the model to real world test sets. * `model_confidence`: - This parameter allows the user to configure how confidences are computed during inference. It can take two values: - * `softmax`: Confidences are in the range `[0, 1]` (old behavior and current default). Computed similarities are normalized with the `softmax` activation function. - * `linear_norm`: Confidences are in the range `[0, 1]`. Computed dot product similarities are normalized with a linear function. - - Please try using `linear_norm` as the value for `model_confidence`. This should make it easier to tune fallback thresholds for the [FallbackClassifier](./components.mdx#fallbackclassifier). - + This parameter allows the user to configure how confidences are computed during inference. It can take only + one value as input which is `softmax`. In `softmax`, confidences are in the range `[0, 1]`. The computed + similarities are normalized with the `softmax` activation function. + The above configuration parameters are the ones you should configure to fit your model to your data. However, additional parameters exist that can be adapted. @@ -2596,12 +2594,9 @@ Selectors predict a bot response from a set of candidate responses. This should help in better generalization of the model to real world test sets. * `model_confidence`: - This parameter allows the user to configure how confidences are computed during inference. It can take two values: - * `softmax`: Confidences are in the range `[0, 1]` (old behavior and current default). Computed similarities are normalized with the `softmax` activation function. - * `linear_norm`: Confidences are in the range `[0, 1]`. Computed dot product similarities are normalized with a linear function. - - Please try using `linear_norm` as the value for `model_confidence`. This should make it easier to tune fallback thresholds for the [FallbackClassifier](./components.mdx#fallbackclassifier). - + This parameter allows the user to configure how confidences are computed during inference. It can take only + one value as input which is `softmax`. In `softmax`, confidences are in the range `[0, 1]`. The computed + similarities are normalized with the `softmax` activation function. The component can also be configured to train a response selector for a particular retrieval intent. The parameter `retrieval_intent` sets the name of the retrieval intent for which this response selector model is trained. From aa9ff823efd09ec20e01d5b37114f6f85d292c0e Mon Sep 17 00:00:00 2001 From: m-vdb Date: Mon, 21 Feb 2022 15:07:11 +0100 Subject: [PATCH 10/65] enable mypy attr-defined check --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 22ed56bddf9b..d8d9a24dd75b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,4 +48,4 @@ disallow_untyped_decorators = True # see https://github.com/RasaHQ/rasa/pull/6470 # the list below is sorted by the number of errors for each error code, in decreasing order disable_error_code = arg-type, assignment, var-annotated, union-attr, - override, attr-defined, misc + override, misc From 98b98e08084a4296fac0782b9003d825eb8ade4b Mon Sep 17 00:00:00 2001 From: m-vdb Date: Mon, 21 Feb 2022 15:51:00 +0100 Subject: [PATCH 11/65] fix Sanic type issues --- stubs/sanic/__init__.pyi | 16 +++++++++++++++- stubs/sanic/app.pyi | 9 ++++----- stubs/sanic/exceptions.pyi | 3 --- 3 files changed, 19 insertions(+), 9 deletions(-) delete mode 100644 stubs/sanic/exceptions.pyi diff --git a/stubs/sanic/__init__.pyi b/stubs/sanic/__init__.pyi index cd253e0281fd..610d5112a38e 100644 --- a/stubs/sanic/__init__.pyi +++ b/stubs/sanic/__init__.pyi @@ -1,4 +1,18 @@ from sanic.__version__ import __version__ from sanic.app import Sanic +from sanic.blueprints import Blueprint +from sanic.constants import HTTPMethod +from sanic.request import Request +from sanic.response import HTTPResponse, html, json, text -__all__ = ["Sanic", "__version__"] +__all__ = [ + "__version__", + "Sanic", + "Blueprint", + "HTTPMethod", + "HTTPResponse", + "Request", + "html", + "json", + "text", +] diff --git a/stubs/sanic/app.pyi b/stubs/sanic/app.pyi index 7e69b7a91abe..7f7b4154e0ec 100644 --- a/stubs/sanic/app.pyi +++ b/stubs/sanic/app.pyi @@ -1,7 +1,6 @@ -from sanic.app import Sanic as SanicSanic - +# mypy check fails here but it actually successfully loads the initial module +# so it's probably an internal issue of mypy with no repercussions +from sanic.app import Sanic as SanicSanic # type: ignore[attr-defined] class Sanic(SanicSanic): - - def stop(self) -> None: - ... + def stop(self) -> None: ... diff --git a/stubs/sanic/exceptions.pyi b/stubs/sanic/exceptions.pyi deleted file mode 100644 index e7f461144d50..000000000000 --- a/stubs/sanic/exceptions.pyi +++ /dev/null @@ -1,3 +0,0 @@ -from typing import NoReturn, Optional, Text - -def abort(status_code: int, message: Optional[Text] = None) -> NoReturn: ... From fac6f8df8611c0b8227bb8039d5fe92aa60eb157 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Mon, 21 Feb 2022 17:22:04 +0100 Subject: [PATCH 12/65] fix Markers type issues --- rasa/core/evaluation/marker_base.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/rasa/core/evaluation/marker_base.py b/rasa/core/evaluation/marker_base.py index 225af97c7031..b2d680846c45 100644 --- a/rasa/core/evaluation/marker_base.py +++ b/rasa/core/evaluation/marker_base.py @@ -11,6 +11,7 @@ Tuple, Type, TypeVar, + TYPE_CHECKING, Union, Any, ) @@ -36,6 +37,9 @@ import csv import os.path +if TYPE_CHECKING: + from rasa.core.evaluation.marker import OrMarker + logger = logging.getLogger(__name__) @@ -273,9 +277,7 @@ def max_depth(self) -> int: """Gets the maximum depth from this point in the marker tree.""" ... - def evaluate_events( - self, events: List[Event], recursive: bool = False - ) -> List[SessionEvaluation]: + def evaluate_events(self, events: List[Event]) -> List[SessionEvaluation]: """Resets the marker, tracks all events, and collects some information. The collected information includes: @@ -285,21 +287,15 @@ def evaluate_events( If this marker is the special `ANY_MARKER` (identified by its name), then results will be collected for all (immediate) sub-markers. - If `recursive` is set to `True`, then all included markers are evaluated. - Args: events: a list of events describing a conversation - recursive: set this to `True` to collect evaluations for all markers that - this marker consists of Returns: a list that contains, for each session contained in the tracker, a dictionary mapping that maps marker names to meta data of relevant events """ # determine which marker to extract results from - if recursive: - markers_to_be_evaluated = [marker for marker in self] - elif isinstance(self, OperatorMarker) and self.name == Marker.ANY_MARKER: + if isinstance(self, OperatorMarker) and self.name == Marker.ANY_MARKER: markers_to_be_evaluated = self.sub_markers else: markers_to_be_evaluated = [self] @@ -395,7 +391,7 @@ def relevant_events(self) -> List[int]: return [idx for (idx, applies) in enumerate(self.history) if applies] @classmethod - def from_path(cls, path: Union[Path, Text]) -> Marker: + def from_path(cls, path: Union[Path, Text]) -> "OrMarker": """Loads markers from one config file or all config files in a directory tree. Each config file should contain a dictionary mapping marker names to the From 4e06c84217b0660b6b82afcea720d0b0b9e074a7 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 09:19:39 +0100 Subject: [PATCH 13/65] fix some [attr-defined] type error --- rasa/nlu/persistor.py | 4 +++- rasa/shared/core/generator.py | 2 +- rasa/shared/core/slot_mappings.py | 2 +- rasa/shared/core/slots.py | 8 ++++++-- rasa/shared/core/trackers.py | 3 +++ rasa/shared/exceptions.py | 6 ++++-- rasa/utils/common.py | 1 + 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/rasa/nlu/persistor.py b/rasa/nlu/persistor.py index 33a1d497af49..f933d528e584 100644 --- a/rasa/nlu/persistor.py +++ b/rasa/nlu/persistor.py @@ -162,7 +162,9 @@ class GCSPersistor(Persistor): Fetches them when needed, instead of storing them on the local disk.""" def __init__(self, bucket_name: Text) -> None: - from google.cloud import storage + # there are no type hints in this repo for now + # https://github.com/googleapis/python-storage/issues/393 + from google.cloud import storage # type: ignore[attr-defined] super().__init__() diff --git a/rasa/shared/core/generator.py b/rasa/shared/core/generator.py index 3b6d0c4c02a1..fa681162f363 100644 --- a/rasa/shared/core/generator.py +++ b/rasa/shared/core/generator.py @@ -61,7 +61,7 @@ def __init__( super().__init__( sender_id, slots, max_event_history, is_rule_tracker=is_rule_tracker ) - self._states_for_hashing = None + self._states_for_hashing: Optional[Deque[FrozenState]] = None self.domain = domain # T/F property to filter augmented stories self.is_augmented = is_augmented diff --git a/rasa/shared/core/slot_mappings.py b/rasa/shared/core/slot_mappings.py index 18eb65c6e3bf..94f3234434d3 100644 --- a/rasa/shared/core/slot_mappings.py +++ b/rasa/shared/core/slot_mappings.py @@ -62,7 +62,7 @@ def validate(mapping: Dict[Text, Any], slot_name: Text) -> None: SlotMappingType.CUSTOM: [], } - required_keys = validations.get(mapping_type) + required_keys = validations[mapping_type] for required_key in required_keys: if mapping.get(required_key) is None: raise InvalidDomain( diff --git a/rasa/shared/core/slots.py b/rasa/shared/core/slots.py index bb3496b3aaca..b7b73bcf5e6d 100644 --- a/rasa/shared/core/slots.py +++ b/rasa/shared/core/slots.py @@ -282,7 +282,11 @@ def _as_feature(self) -> List[float]: # we couldn't convert the value to a list - using default value return [0.0] - @Slot.value.setter + @property + def value(self) -> Any: + return super().value + + @value.setter def value(self, value: Any) -> None: """Sets the slot's value.""" if value and not isinstance(value, list): @@ -290,7 +294,7 @@ def value(self, value: Any) -> None: value = [value] # Call property setter of superclass - super(ListSlot, self.__class__).value.fset(self, value) + super().value.fset(self, value) class CategoricalSlot(Slot): diff --git a/rasa/shared/core/trackers.py b/rasa/shared/core/trackers.py index f9982fb6505b..82bd2b4287b2 100644 --- a/rasa/shared/core/trackers.py +++ b/rasa/shared/core/trackers.py @@ -843,6 +843,9 @@ def latest_action_name(self) -> Optional[Text]: Returns: name of the previously executed action or text of e2e action """ + if self.latest_action is None: + return None + return self.latest_action.get(ACTION_NAME) or self.latest_action.get( ACTION_TEXT ) diff --git a/rasa/shared/exceptions.py b/rasa/shared/exceptions.py index 19b983b940ee..d84373f1f03a 100644 --- a/rasa/shared/exceptions.py +++ b/rasa/shared/exceptions.py @@ -54,8 +54,10 @@ def __str__(self) -> Text: exception_text = "Failed to read YAML." if self.underlying_yaml_exception: - self.underlying_yaml_exception.warn = None - self.underlying_yaml_exception.note = None + if hasattr(self.underlying_yaml_exception, "warn"): + self.underlying_yaml_exception.warn = None + if hasattr(self.underlying_yaml_exception, "note"): + self.underlying_yaml_exception.note = None exception_text += f" {self.underlying_yaml_exception}" if self.filename: diff --git a/rasa/utils/common.py b/rasa/utils/common.py index 9397239ba81f..9a8c42f9f018 100644 --- a/rasa/utils/common.py +++ b/rasa/utils/common.py @@ -1,6 +1,7 @@ import asyncio import copy import logging +import logging.handlers import os import shutil import warnings From d929a8c59dcb7fefcaa1d6a3f60e617d5c7ab185 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 09:25:32 +0100 Subject: [PATCH 14/65] use official types-redis library --- poetry.lock | 49 +++++++++++++++++++++++------------------ pyproject.toml | 1 + stubs/redis/__init__.py | 43 ------------------------------------ 3 files changed, 28 insertions(+), 65 deletions(-) delete mode 100644 stubs/redis/__init__.py diff --git a/poetry.lock b/poetry.lock index be0509a2673b..681f097218b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3241,6 +3241,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "types-redis" +version = "4.1.16" +description = "Typing stubs for redis" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "types-requests" version = "2.27.9" @@ -3446,7 +3454,7 @@ transformers = ["transformers"] [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.9" -content-hash = "d58b68d407a8771ba07902dec0c0dc466b4ea2a25ecb5332a445ca37b9d84250" +content-hash = "1b1a911765829804744185d133017a4dd38f687cff9eeddeff9e928af2a3f2a5" [metadata.files] absl-py = [ @@ -3569,7 +3577,7 @@ black = [ {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, ] blis = [ - {file = "blis-0.7.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:98eba77b1e1fde7813bc0453ab78b6ae2067f5bc0fe9e3abc671b2895cfecf33"}, + {file = "blis-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5812a7c04561ae7332cf730f57d9f82cbd12c5f86a5bfad66ee244e51d06266d"}, {file = "blis-0.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eecfce3d8fce61dede7b0ae0dffa461c22072437b6cde85587db0c1aa75b450"}, {file = "blis-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:0e476931f0d5703a21c77e7f69b8ebdeeea493fc7858a86f627ac2b376a12c8d"}, {file = "blis-0.7.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5966ddf3bce84aa7bb09ce4ca059309602fa63280a5d5e5365bb2a294bd5a138"}, @@ -3774,7 +3782,7 @@ cycler = [ {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, ] cymem = [ - {file = "cymem-2.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b4e27e739f09f16c7c0190f962ffe60dab39cb6a229d5c13e274d16f46a17e8"}, + {file = "cymem-2.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:700540b68e96a7056d0691d467df2bbaaf0934a3e6fe2383669998cbee19580a"}, {file = "cymem-2.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:971cf0a8437dfb4185c3049c086e463612fe849efadc0f5cc153fc81c501da7d"}, {file = "cymem-2.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:6b0d1a6b0a1296f31fa9e4b7ae5ea49394084ecc883b1ae6fec4844403c43468"}, {file = "cymem-2.0.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b8e1c18bb00800425576710468299153caad20c64ddb6819d40a6a34e21ee21c"}, @@ -4257,9 +4265,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, @@ -4271,9 +4276,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -4285,9 +4287,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, @@ -4300,9 +4299,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -4315,9 +4311,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -4486,7 +4479,7 @@ multidict = [ {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, ] murmurhash = [ - {file = "murmurhash-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a814d559afe2a97ad40accf21ce96e8b04a3ff5a08f80c02b7acd427dbb7d567"}, + {file = "murmurhash-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1431d817e1fff1ed35f8dc54dd5b4d70165ec98076de8aca351805f8037293f3"}, {file = "murmurhash-1.0.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7b8cc4a8db1c821b80f8ca70a25c3166b14d68ecef8693a117c6a0b1d74ace"}, {file = "murmurhash-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:e40790fdaf65213d70da4ed9229f16f6d6376310dc8fc23eacc98e6151c6ae7e"}, {file = "murmurhash-1.0.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a78d53f047c3410ce4c589d9b47090f628f844ed5694418144e63cfe7f3da7e9"}, @@ -4689,7 +4682,7 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] preshed = [ - {file = "preshed-3.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9683730127658b531120b4ed5cff1f2a567318ab75e9ab0f22cc84ae1486c23"}, + {file = "preshed-3.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66a71ced487516cf81fd0431a3a843514262ae2f33e9a7688b87562258fa75d5"}, {file = "preshed-3.0.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c98f725d8478f3ade4ab1ea00f50a92d2d9406d37276bc46fd8bab1d47452c4"}, {file = "preshed-3.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:ea8aa9610837e907e8442e79300df0a861bfdb4dcaf026a5d9642a688ad04815"}, {file = "preshed-3.0.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e03ae3eee961106a517fcd827b5a7c51f7317236b3e665c989054ab8dc381d28"}, @@ -4751,6 +4744,11 @@ psutil = [ {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2"}, {file = "psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d"}, {file = "psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b"}, + {file = "psutil-5.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d"}, + {file = "psutil-5.9.0-cp36-cp36m-win32.whl", hash = "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64"}, + {file = "psutil-5.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94"}, {file = "psutil-5.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0"}, {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce"}, {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5"}, @@ -5472,7 +5470,7 @@ sqlalchemy = [ {file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"}, ] srsly = [ - {file = "srsly-2.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:834229df7377386e9990fd245e1ae12a72152997fd159a782a798b638721a7b2"}, + {file = "srsly-2.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e22bbc1a20abf749fa53adf101c36bc369ec63f496c7a44bf4f5f287d724900"}, {file = "srsly-2.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004d29a5abc0fe632434359c0be170490a69c4dce2c3de8a769944c37da7bb4b"}, {file = "srsly-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7ced7ec4993b4d4ad73cc442f8f7a518368348054d510864b1aa149e8d71654d"}, {file = "srsly-2.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:801c7e6e32c6a4721ab78ab7dafd01074fdb144f4876c09b25305c98f95c470f"}, @@ -5546,12 +5544,15 @@ tensorflow-io-gcs-filesystem = [ {file = "tensorflow_io_gcs_filesystem-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:2f67d19a2f2579dc55f1590faf48c2e882cabb860992b5a9c7edb0ed8b3eb187"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cde835e68b2b43ddade07c999e7c3251bcd62b1ff165c34fbe9fc6e0f12c3ac9"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:71c00638c9b6048480095f2738dfefd8f4b2e7b534190c91d699aee769bfa86e"}, + {file = "tensorflow_io_gcs_filesystem-0.24.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f63d70d7fce10c63f21bdd8e72244958afc0c495966831a547f038543c9633f7"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d1eb5e9be62040c5a249ae8adaae7e61f65b59541139e4d6767157f25a224bf5"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:cc093f160f79526d31f6070a3ddc000868d737a36ccf40984128661563383601"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6e65009770a05a3b55c5f782348f785e5034d277a727832811ad737bd857c8c9"}, + {file = "tensorflow_io_gcs_filesystem-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658764aaaf9419ddefb3daa95bdc84e5210c691ff73b8ac2606d5c839040206b"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:aa90b9a34ea8da4dbd534f77746d67375714db869524da889193c3042352679a"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:825f396388748038ad38c35b091311982081f93a5db8ca9763fc874c3f555e6c"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cbc71b3925508bf796644a0083a6f9284f71404654f53092bece701383a69520"}, + {file = "tensorflow_io_gcs_filesystem-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae96b20b973b1c3bbf2c068409035ead45177447ef51701f4e726f67cadc4695"}, {file = "tensorflow_io_gcs_filesystem-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:2862e0869453ce1f872a28d1362768ee078ec227ea587dd69164081dea6d7177"}, ] tensorflow-text = [ @@ -5573,7 +5574,7 @@ terminaltables = [ {file = "terminaltables-3.1.10.tar.gz", hash = "sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543"}, ] thinc = [ - {file = "thinc-8.0.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad8794a76725b85847528fd1a56471d5ac00f4104da8efb065ba572238e381a2"}, + {file = "thinc-8.0.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f818b9f012169a11beb3561c43dc52080588e50cf495733e492efab8b9b4135e"}, {file = "thinc-8.0.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f520daf45b7f42a04363852df43be1b423ae42d9327709d74f6c3279b3f73778"}, {file = "thinc-8.0.13-cp310-cp310-win_amd64.whl", hash = "sha256:2b217059c9e126220b77e7d6c9da56912c4e1eb4e8a11af14f17752e198e88cc"}, {file = "thinc-8.0.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0f956c693d180209075703072fd226a24408cbe80eb67bd3b6eea407f61cb283"}, @@ -5692,6 +5693,10 @@ types-pytz = [ {file = "types-pytz-2021.3.5.tar.gz", hash = "sha256:fef8de238ee95135952229a2a23bfb87bd63d5a6c8598106a46cfcf48f069ea8"}, {file = "types_pytz-2021.3.5-py3-none-any.whl", hash = "sha256:8831f689379ac9e2a62668157381379ed74b3702980e08e71f8673c179c4e3c7"}, ] +types-redis = [ + {file = "types-redis-4.1.16.tar.gz", hash = "sha256:a913521c1f008775fc3816813a5981f9da3b0dd3f3b2578b0a0464a84ac5f4d4"}, + {file = "types_redis-4.1.16-py3-none-any.whl", hash = "sha256:a529fbae3b6c95e6790522d35a3065dc92ee29698c6b163ab573992b6144b41a"}, +] types-requests = [ {file = "types-requests-2.27.9.tar.gz", hash = "sha256:7368974534d297939492efdfdab232930440b11e2203f6df1f0c40e3242a87ea"}, {file = "types_requests-2.27.9-py3-none-any.whl", hash = "sha256:74070045418faf710f3154403d6a16c9e67db50e5119906ca6955f1658d20f7b"}, diff --git a/pyproject.toml b/pyproject.toml index 6a10f3c9f359..b01130b8ea1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,6 +164,7 @@ sanic-testing = "^0.8.0" analytics-python = "^1.4.0" datadog-api-client = "^1.7.0" datadog = "^0.43.0" +types-redis = "^4.1.16" [tool.poetry.extras] spacy = [ "spacy",] diff --git a/stubs/redis/__init__.py b/stubs/redis/__init__.py deleted file mode 100644 index bd0758a85a8c..000000000000 --- a/stubs/redis/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Text, List, overload, Optional, Union, Mapping, Literal - -from redis import ConnectionPool -from typing_extensions import Protocol - -# We should switch to https://pypi.org/project/types-redis/ once -# https://github.com/python/typeshed/issues/5065 is fixed. -class StrictRedis(Protocol): - @overload - def __init__( - self, - host: Text = ..., - port: int = ..., - db: int = ..., - password: Optional[Text] = ..., - socket_timeout: Optional[float] = ..., - socket_connect_timeout: Optional[float] = ..., - socket_keepalive: Optional[bool] = ..., - socket_keepalive_options: Optional[Mapping[str, Union[int, str]]] = ..., - connection_pool: Optional[ConnectionPool] = ..., - unix_socket_path: Optional[Text] = ..., - encoding: Text = ..., - encoding_errors: Text = ..., - charset: Optional[Text] = ..., - errors: Optional[Text] = ..., - decode_responses: Literal[False] = ..., - retry_on_timeout: bool = ..., - ssl: bool = ..., - ssl_keyfile: Optional[Text] = ..., - ssl_certfile: Optional[Text] = ..., - ssl_cert_reqs: Optional[Union[str, int]] = ..., - ssl_ca_certs: Optional[Text] = ..., - ssl_check_hostname: bool = ..., - max_connections: Optional[int] = ..., - single_connection_client: bool = ..., - health_check_interval: float = ..., - client_name: Optional[Text] = ..., - username: Optional[Text] = ..., - ) -> None: - ... - - def keys(self, pattern: Text) -> List[Text]: - ... From fe2fb3b58d4ba06aba451c5e027cb938d70851ac Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 11:19:53 +0100 Subject: [PATCH 15/65] fix more [attr-defined] type error --- rasa/core/actions/forms.py | 41 +++++++++---------- rasa/core/agent.py | 4 +- rasa/core/brokers/kafka.py | 13 +++--- rasa/core/channels/slack.py | 10 +++-- rasa/core/channels/socketio.py | 16 ++++---- rasa/core/policies/rule_policy.py | 1 + rasa/model_training.py | 13 ++---- .../story_writer/yaml_story_writer.py | 28 ++++++++----- .../core/training_data/visualization.py | 17 +++++--- rasa/utils/tensorflow/model_data_utils.py | 14 +++---- 10 files changed, 85 insertions(+), 72 deletions(-) diff --git a/rasa/core/actions/forms.py b/rasa/core/actions/forms.py index d1a66c273995..bcb4c8b44c54 100644 --- a/rasa/core/actions/forms.py +++ b/rasa/core/actions/forms.py @@ -504,8 +504,8 @@ async def _ask_for_slot( ) -> List[Event]: logger.debug(f"Request next slot '{slot_name}'") - action_to_ask_for_next_slot = self._name_of_utterance(domain, slot_name) - if not action_to_ask_for_next_slot: + action_name_to_ask_for_next_slot = self._name_of_utterance(domain, slot_name) + if not action_name_to_ask_for_next_slot: # Use a debug log as the user might have asked as part of a custom action logger.debug( f"There was no action found to ask for slot '{slot_name}' " @@ -514,7 +514,7 @@ async def _ask_for_slot( return [] action_to_ask_for_next_slot = action.action_for_name_or_text( - action_to_ask_for_next_slot, domain, self.action_endpoint + action_name_to_ask_for_next_slot, domain, self.action_endpoint ) return await action_to_ask_for_next_slot.run( output_channel, nlg, tracker, domain @@ -631,25 +631,22 @@ async def is_done( # We explicitly check only the last occurrences for each possible termination # event instead of doing `return event in events_so_far` to make it possible # to override termination events which were returned earlier. - return ( - next( - ( - event - for event in reversed(events_so_far) - if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT - ), - None, - ) - == SlotSet(REQUESTED_SLOT, None) - or next( - ( - event - for event in reversed(events_so_far) - if isinstance(event, ActiveLoop) - ), - None, - ) - == ActiveLoop(None) + return next( + ( + event + for event in reversed(events_so_far) + if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT + ), + None, + ) == SlotSet(REQUESTED_SLOT, None) or next( + ( + event + for event in reversed(events_so_far) + if isinstance(event, ActiveLoop) + ), + None, + ) == ActiveLoop( + None ) async def deactivate(self, *args: Any, **kwargs: Any) -> List[Event]: diff --git a/rasa/core/agent.py b/rasa/core/agent.py index 444cb67b76e0..9813ec3a1eed 100644 --- a/rasa/core/agent.py +++ b/rasa/core/agent.py @@ -19,7 +19,7 @@ from rasa.core.exceptions import AgentNotReady from rasa.shared.constants import DEFAULT_SENDER_ID from rasa.core.lock_store import InMemoryLockStore, LockStore -from rasa.core.nlg import NaturalLanguageGenerator +from rasa.core.nlg import NaturalLanguageGenerator, TemplatedNaturalLanguageGenerator from rasa.core.policies.policy import PolicyPrediction from rasa.core.processor import MessageProcessor from rasa.core.tracker_store import FailSafeTrackerStore, InMemoryTrackerStore @@ -363,7 +363,7 @@ def load_model( # update domain on all instances self.tracker_store.domain = self.domain - if hasattr(self.nlg, "responses"): + if isinstance(self.nlg, TemplatedNaturalLanguageGenerator): self.nlg.responses = self.domain.responses if self.domain else {} @property diff --git a/rasa/core/brokers/kafka.py b/rasa/core/brokers/kafka.py index 878d93e381b6..53512270670c 100644 --- a/rasa/core/brokers/kafka.py +++ b/rasa/core/brokers/kafka.py @@ -2,7 +2,7 @@ import json import logging from asyncio import AbstractEventLoop -from typing import Any, Text, List, Optional, Union, Dict +from typing import Any, Text, List, Optional, Union, Dict, TYPE_CHECKING import time from rasa.core.brokers.broker import EventBroker @@ -11,6 +11,9 @@ from rasa.shared.exceptions import RasaException import rasa.shared.utils.common +if TYPE_CHECKING: + from kafka import KafkaProducer + logger = logging.getLogger(__name__) @@ -105,7 +108,7 @@ def publish( ) -> None: """Publishes events.""" if self.producer is None: - self._create_producer() + self.producer = self._create_producer() connected = self.producer.bootstrap_connected() if connected: logger.debug("Connection to kafka successful.") @@ -125,7 +128,7 @@ def publish( if not connected: self._close() logger.debug("Connection to kafka lost, reconnecting...") - self._create_producer() + self.producer = self._create_producer() connected = self.producer.bootstrap_connected() if connected: logger.debug("Reconnection to kafka successful") @@ -135,7 +138,7 @@ def publish( logger.error("Failed to publish Kafka event.") - def _create_producer(self) -> None: + def _create_producer(self) -> "KafkaProducer": import kafka if self.security_protocol == "PLAINTEXT": @@ -175,7 +178,7 @@ def _create_producer(self) -> None: ) try: - self.producer = kafka.KafkaProducer( + return kafka.KafkaProducer( client_id=self.client_id, bootstrap_servers=self.url, value_serializer=lambda v: json.dumps(v).encode(DEFAULT_ENCODING), diff --git a/rasa/core/channels/slack.py b/rasa/core/channels/slack.py index 4a8570368d89..644bd09a6c3f 100644 --- a/rasa/core/channels/slack.py +++ b/rasa/core/channels/slack.py @@ -93,15 +93,17 @@ async def send_text_with_buttons( ) return await self.send_text_message(recipient, text, **kwargs) - button_block = {"type": "actions", "elements": []} - for button in buttons: - button_block["elements"].append( + button_block = { + "type": "actions", + "elements": [ { "type": "button", "text": {"type": "plain_text", "text": button["title"]}, "value": button["payload"], } - ) + for button in buttons + ], + } await self._post_message( channel=recipient, diff --git a/rasa/core/channels/socketio.py b/rasa/core/channels/socketio.py index 3753a8fac897..70474f9320c4 100644 --- a/rasa/core/channels/socketio.py +++ b/rasa/core/channels/socketio.py @@ -83,14 +83,14 @@ async def send_text_with_buttons( messages = [{"text": message, "quick_replies": []} for message in message_parts] # attach all buttons to the last text fragment - for button in buttons: - messages[-1]["quick_replies"].append( - { - "content_type": "text", - "title": button["title"], - "payload": button["payload"], - } - ) + messages[-1]["quick_replies"] = [ + { + "content_type": "text", + "title": button["title"], + "payload": button["payload"], + } + for button in buttons + ] for message in messages: await self._send_message(recipient_id, message) diff --git a/rasa/core/policies/rule_policy.py b/rasa/core/policies/rule_policy.py index f2c7251ea074..b16884baa5c2 100644 --- a/rasa/core/policies/rule_policy.py +++ b/rasa/core/policies/rule_policy.py @@ -946,6 +946,7 @@ def _find_action_from_loop_happy_path( active_loop_rejected = tracker.active_loop.get(LOOP_REJECTED) should_predict_loop = ( not active_loop_rejected + and tracker.latest_action and tracker.latest_action.get(ACTION_NAME) != active_loop_name ) should_predict_listen = ( diff --git a/rasa/model_training.py b/rasa/model_training.py index a01eac75fa12..7b5ba9945168 100644 --- a/rasa/model_training.py +++ b/rasa/model_training.py @@ -134,8 +134,8 @@ def train( ) return TrainingResult(code=1) - domain = file_importer.get_domain() - if domain.is_empty(): + domain_object = file_importer.get_domain() + if domain_object.is_empty(): rasa.shared.utils.cli.print_warning( "Core training was skipped because no valid domain file was found. " "Only an NLU-model was created. Please specify a valid domain using " @@ -200,15 +200,10 @@ def _train_graph( config = file_importer.get_config() recipe = Recipe.recipe_for_name(config.get("recipe")) config, _missing_keys, _configured_keys = recipe.auto_configure( - file_importer.get_config_file_for_auto_config(), - config, - training_type, + file_importer.get_config_file_for_auto_config(), config, training_type, ) model_configuration = recipe.graph_config_for_recipe( - config, - kwargs, - training_type=training_type, - is_finetuning=is_finetuning, + config, kwargs, training_type=training_type, is_finetuning=is_finetuning, ) rasa.engine.validation.validate(model_configuration) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index 24645c0a7364..e04debaa443a 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -213,9 +213,11 @@ def process_user_utterance( for entity in user_utterance.entities: if "value" in entity: if hasattr(user_utterance, "inline_comment_for_entity"): - for predicted in user_utterance.predicted_entities: + # FIXME: to fix this type issue, WronglyClassifiedUserUtterance needs to + # be imported but it's currently outside of `rasa.shared` + for predicted in user_utterance.predicted_entities: # type: ignore[attr-defined] if predicted["start"] == entity["start"]: - commented_entity = user_utterance.inline_comment_for_entity( # noqa: E501 + commented_entity = user_utterance.inline_comment_for_entity( # type: ignore[attr-defined] predicted, entity ) if commented_entity: @@ -241,7 +243,9 @@ def process_user_utterance( result[KEY_ENTITIES] = entities if hasattr(user_utterance, "inline_comment"): - comment = user_utterance.inline_comment( + # FIXME: to fix this type issue, WronglyClassifiedUserUtterance needs to + # be imported but it's currently outside of `rasa.shared` + comment = user_utterance.inline_comment( # type: ignore[attr-defined] force_comment_generation=not entities ) if comment: @@ -283,7 +287,9 @@ def process_action(action: ActionExecuted) -> Optional[OrderedDict]: result[KEY_BOT_END_TO_END_MESSAGE] = action.action_text if hasattr(action, "inline_comment"): - comment = action.inline_comment() + # FIXME: to fix this type issue, WarningPredictedAction needs to + # be imported but it's currently outside of `rasa.shared` + comment = action.inline_comment() # type: ignore[attr-defined] if KEY_ACTION in result and comment: result.yaml_add_eol_comment(comment, KEY_ACTION) elif KEY_BOT_END_TO_END_MESSAGE in result and comment: @@ -395,11 +401,13 @@ def process_rule_step(self, rule_step: RuleStep) -> OrderedDict: if normal_steps: result[KEY_STEPS] = normal_steps - if len(normal_events) > 1 and ( - isinstance(normal_events[len(normal_events) - 1], ActionExecuted) - and normal_events[len(normal_events) - 1].action_name - == rasa.shared.core.constants.RULE_SNIPPET_ACTION_NAME - ): - result[KEY_WAIT_FOR_USER_INPUT_AFTER_RULE] = False + if len(normal_events) > 1: + last_event = normal_events[len(normal_events) - 1] + if ( + isinstance(last_event, ActionExecuted) + and last_event.action_name + == rasa.shared.core.constants.RULE_SNIPPET_ACTION_NAME + ): + result[KEY_WAIT_FOR_USER_INPUT_AFTER_RULE] = False return result diff --git a/rasa/shared/core/training_data/visualization.py b/rasa/shared/core/training_data/visualization.py index a237ac2f63a4..ae4f2fb2e79b 100644 --- a/rasa/shared/core/training_data/visualization.py +++ b/rasa/shared/core/training_data/visualization.py @@ -1,7 +1,7 @@ from collections import defaultdict, deque import random -from typing import Any, Text, List, Dict, Optional, TYPE_CHECKING, Set +from typing import Any, Text, List, Dict, Optional, Set, TYPE_CHECKING, Union, cast import rasa.shared.utils.io from rasa.shared.constants import INTENT_MESSAGE_PREFIX @@ -326,8 +326,14 @@ def _length_of_common_action_prefix(this: List[Event], other: List[Event]) -> in """Calculate number of actions that two conversations have in common.""" num_common_actions = 0 - t_cleaned = [e for e in this if e.type_name in {"user", "action"}] - o_cleaned = [e for e in other if e.type_name in {"user", "action"}] + t_cleaned = cast( + List[Union[ActionExecuted, UserUttered]], + [e for e in this if e.type_name in {"user", "action"}], + ) + o_cleaned = cast( + List[Union[ActionExecuted, UserUttered]], + [e for e in other if e.type_name in {"user", "action"}], + ) for i, e in enumerate(t_cleaned): if i == len(o_cleaned): @@ -462,9 +468,10 @@ def visualize_neighborhood( # this can either be an ellipsis "...", the conversation end node # "END" or a "TMP" node if this is the active conversation if is_current: + event_idx = events[idx] if ( - isinstance(events[idx], ActionExecuted) - and events[idx].action_name == ACTION_LISTEN_NAME + isinstance(event_idx, ActionExecuted) + and event_idx.action_name == ACTION_LISTEN_NAME ): next_node_idx += 1 graph.add_node( diff --git a/rasa/utils/tensorflow/model_data_utils.py b/rasa/utils/tensorflow/model_data_utils.py index f2c4a96d451e..cba475d8346a 100644 --- a/rasa/utils/tensorflow/model_data_utils.py +++ b/rasa/utils/tensorflow/model_data_utils.py @@ -458,25 +458,25 @@ def _extract_features( attribute_mask[i] = 0 list_of_features = fake_features - for features in list_of_features: + for feature in list_of_features: # in case of ENTITIES, if the attribute type matches either 'entity', # 'role', or 'group' the features correspond to the tag ids of that # entity type in order to distinguish later on between the different # tag ids, we use the entity type as key - if attribute == ENTITIES and features.attribute in [ + if attribute == ENTITIES and feature.attribute in [ ENTITY_ATTRIBUTE_TYPE, ENTITY_ATTRIBUTE_GROUP, ENTITY_ATTRIBUTE_ROLE, ]: - key = features.attribute + key = feature.attribute else: - key = features.type + key = feature.type # all features should have the same types - if features.is_sparse(): - dialogue_sparse_features[key].append(features.features) + if feature.is_sparse(): + dialogue_sparse_features[key].append(feature.features) else: - dialogue_dense_features[key].append(features.features) + dialogue_dense_features[key].append(feature.features) for key, value in dialogue_sparse_features.items(): sparse_features[key].append(value) From 6402a091e66abb698ee84467c301207659930e62 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 11:25:43 +0100 Subject: [PATCH 16/65] fix ruamel type issues --- rasa/shared/exceptions.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/rasa/shared/exceptions.py b/rasa/shared/exceptions.py index d84373f1f03a..d5ba847054bb 100644 --- a/rasa/shared/exceptions.py +++ b/rasa/shared/exceptions.py @@ -2,6 +2,11 @@ from typing import Optional, Text import jsonschema +from ruamel.yaml.error import ( + MarkedYAMLError, + MarkedYAMLWarning, + MarkedYAMLFutureWarning, +) class RasaException(Exception): @@ -54,10 +59,16 @@ def __str__(self) -> Text: exception_text = "Failed to read YAML." if self.underlying_yaml_exception: - if hasattr(self.underlying_yaml_exception, "warn"): - self.underlying_yaml_exception.warn = None - if hasattr(self.underlying_yaml_exception, "note"): + if isinstance( + self.underlying_yaml_exception, + (MarkedYAMLError, MarkedYAMLWarning, MarkedYAMLFutureWarning), + ): self.underlying_yaml_exception.note = None + if isinstance( + self.underlying_yaml_exception, + (MarkedYAMLWarning, MarkedYAMLFutureWarning), + ): + self.underlying_yaml_exception.warn = None exception_text += f" {self.underlying_yaml_exception}" if self.filename: From fcae5b64667efe5dd150f6ba838167f62aea6ee2 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 11:36:09 +0100 Subject: [PATCH 17/65] bump mypy version --- poetry.lock | 59 ++++++++++++++++++++++++-------------------------- pyproject.toml | 2 +- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/poetry.lock b/poetry.lock index 681f097218b6..93b54e531430 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1505,21 +1505,21 @@ python-versions = "*" [[package]] name = "mypy" -version = "0.910" +version = "0.931" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.7.4" +mypy-extensions = ">=0.4.3" +tomli = ">=1.1.0" +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] [[package]] name = "mypy-extensions" @@ -3454,7 +3454,7 @@ transformers = ["transformers"] [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.9" -content-hash = "1b1a911765829804744185d133017a4dd38f687cff9eeddeff9e928af2a3f2a5" +content-hash = "a5728f4862dfc3cb154339b6801ccece732aba2422313b16d00a65e55c947e54" [metadata.files] absl-py = [ @@ -4497,29 +4497,26 @@ murmurhash = [ {file = "murmurhash-1.0.6.tar.gz", hash = "sha256:00a5252b569d3f914b5bd0bce72d2efe9c0fb91a9703556ea1b608b141c68f2d"}, ] mypy = [ - {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, - {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, - {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, - {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, - {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, - {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, - {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, - {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, - {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, - {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, - {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, - {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, - {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, - {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, - {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, - {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, - {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, - {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, - {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, - {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, - {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, - {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, - {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, + {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, + {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, + {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, + {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, + {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, + {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, + {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, + {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, + {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, + {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, + {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, + {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, + {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, + {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, + {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, + {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, + {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, + {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, + {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, + {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, diff --git a/pyproject.toml b/pyproject.toml index b01130b8ea1b..d8113e4cdf63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,7 @@ toml = "^0.10.0" pep440-version-utils = "^0.3.0" pydoc-markdown = "^3.10.3" pytest-timeout = "^1.4.2" -mypy = "^0.910" +mypy = "^0.931" bandit = "^1.6.3" types-pkg-resources = "^0.1.3" types-pytz = "^2021.1.0" From f908de199ae6108cffd2ce609e5e31d6f8ceb21f Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 11:46:34 +0100 Subject: [PATCH 18/65] address new mypy issues --- rasa/server.py | 6 +++--- rasa/shared/importers/importer.py | 3 +-- rasa/utils/tensorflow/model_data.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rasa/server.py b/rasa/server.py index 72d64cb9382d..44b54e652ed3 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -80,8 +80,7 @@ response.HTTPResponse, Coroutine[Any, Any, response.HTTPResponse] ] SanicView = Callable[ - [Arg(Request, "request"), VarArg(), KwArg()], # noqa: F821 - SanicResponse, + [Arg(Request, "request"), VarArg(), KwArg()], SanicResponse, # noqa: F821 ] @@ -1364,7 +1363,8 @@ async def unload_model(request: Request) -> HTTPResponse: @ensure_loaded_agent(app) async def get_domain(request: Request) -> HTTPResponse: """Get current domain in yaml or json format.""" - accepts = request.headers.get("Accept", default=JSON_CONTENT_TYPE) + # FIXME: this is a false positive mypy error after upgrading to 0.931 + accepts = request.headers.get("Accept", default=JSON_CONTENT_TYPE) # type: ignore[call-overload] if accepts.endswith("json"): domain = app.ctx.agent.domain.as_dict() return response.json(domain) diff --git a/rasa/shared/importers/importer.py b/rasa/shared/importers/importer.py index dabb9624eb6f..95b9bbc2732e 100644 --- a/rasa/shared/importers/importer.py +++ b/rasa/shared/importers/importer.py @@ -173,8 +173,7 @@ def _importer_from_dict( importer_config, importer_class ) - # mypy ignore needed because RasaFileImporter and MultiFI have different args - return importer_class( # type: ignore[call-arg] + return importer_class( config_path, domain_path, training_data_paths, **constructor_arguments ) diff --git a/rasa/utils/tensorflow/model_data.py b/rasa/utils/tensorflow/model_data.py index 0e0a53d15513..7167ca86156d 100644 --- a/rasa/utils/tensorflow/model_data.py +++ b/rasa/utils/tensorflow/model_data.py @@ -417,7 +417,7 @@ def number_of_units(self, key: Text, sub_key: Text) -> int: units = 0 for features in self.data[key][sub_key]: if len(features) > 0: - units += features.units + units += features.units # type: ignore[operator] return units From 19439e15225f5431de00482e9613437757696ddb Mon Sep 17 00:00:00 2001 From: Melinda Loubser <32034278+melindaloubser1@users.noreply.github.com> Date: Tue, 22 Feb 2022 12:13:30 +0100 Subject: [PATCH 19/65] use constant for training data version everywhere (#10909) * rebase * fix import of constant name * fix quoting * Always use DoubleQuotedScalarString for training data version * Add another case of DoubleQuotedScalarString * missing fstrings --- rasa/core/migrate.py | 15 ++- rasa/shared/core/domain.py | 6 +- tests/cli/test_rasa_data.py | 11 ++- tests/cli/test_rasa_test.py | 2 +- tests/core/actions/test_forms.py | 28 +++--- tests/core/actions/test_two_stage_fallback.py | 7 +- tests/core/nlg/test_response.py | 31 +++--- tests/core/policies/test_rule_policy.py | 97 ++++++++++--------- tests/core/policies/test_ted_policy.py | 6 +- .../policies/test_unexpected_intent_policy.py | 5 +- tests/core/test_actions.py | 74 +++++++------- tests/core/test_evaluation.py | 2 +- tests/core/test_migrate.py | 9 +- tests/core/test_processor.py | 2 +- tests/core/test_test.py | 36 +++---- tests/core/training/test_interactive.py | 3 +- tests/nlu/extractors/test_extractor.py | 11 ++- tests/shared/core/test_domain.py | 80 +++++++-------- tests/shared/core/test_slot_mappings.py | 11 ++- tests/shared/core/test_trackers.py | 4 +- .../story_writer/test_yaml_story_writer.py | 19 ++-- .../core/training_data/test_structures.py | 3 +- tests/shared/utils/test_validation.py | 12 +-- tests/test_model_testing.py | 5 +- tests/test_model_training.py | 5 +- tests/test_server.py | 21 ++-- tests/test_validator.py | 45 ++++----- 27 files changed, 296 insertions(+), 254 deletions(-) diff --git a/rasa/core/migrate.py b/rasa/core/migrate.py index c454d9f444ae..8de15d0fb63b 100644 --- a/rasa/core/migrate.py +++ b/rasa/core/migrate.py @@ -3,6 +3,8 @@ from pathlib import Path from typing import List, Dict, Text, Any, Tuple, Optional, Union +from ruamel.yaml.scalarstring import DoubleQuotedScalarString + import rasa.shared.utils.io import rasa.shared.utils.cli from rasa.shared.constants import REQUIRED_SLOTS_KEY, IGNORED_INTENTS @@ -13,6 +15,7 @@ MAPPING_TYPE, SLOT_MAPPINGS, ) +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.domain import KEY_ENTITIES, KEY_SLOTS, KEY_FORMS, Domain from rasa.shared.exceptions import RasaException @@ -172,7 +175,9 @@ def _assemble_new_domain( elif key == KEY_FORMS: new_domain.update({key: new_forms}) elif key == "version": - new_domain.update({key: '"3.0"'}) + new_domain.update( + {key: DoubleQuotedScalarString(LATEST_TRAINING_DATA_FORMAT_VERSION)} + ) else: new_domain.update({key: value}) return new_domain @@ -226,7 +231,13 @@ def _migrate_domain_files( if KEY_SLOTS not in original_content and KEY_FORMS not in original_content: if isinstance(original_content, dict): - original_content.update({"version": '"3.0"'}) + original_content.update( + { + "version": DoubleQuotedScalarString( + LATEST_TRAINING_DATA_FORMAT_VERSION + ) + } + ) # this is done so that the other domain files can be moved # in the migrated directory diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index 27ed4f99ade9..653596991634 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -19,6 +19,8 @@ Iterable, ) +from ruamel.yaml.scalarstring import DoubleQuotedScalarString + from rasa.shared.constants import ( DEFAULT_SESSION_EXPIRATION_TIME_IN_MINUTES, DEFAULT_CARRY_OVER_SLOTS_TO_NEW_SESSION, @@ -1608,7 +1610,9 @@ def as_yaml(self, clean_before_dump: bool = False) -> Text: # thanks to the `should_preserve_key_order` argument # of `dump_obj_as_yaml_to_string` domain_data: Dict[Text, Any] = { - KEY_TRAINING_DATA_FORMAT_VERSION: LATEST_TRAINING_DATA_FORMAT_VERSION + KEY_TRAINING_DATA_FORMAT_VERSION: DoubleQuotedScalarString( + LATEST_TRAINING_DATA_FORMAT_VERSION + ) } if clean_before_dump: domain_data.update(self.cleaned_domain()) diff --git a/tests/cli/test_rasa_data.py b/tests/cli/test_rasa_data.py index fd671ef95cd3..700d2beaf1e9 100644 --- a/tests/cli/test_rasa_data.py +++ b/tests/cli/test_rasa_data.py @@ -9,6 +9,7 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import RunResult from rasa.cli import data +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.importers.importer import TrainingDataImporter from rasa.validator import Validator import rasa.shared.utils.io @@ -156,7 +157,7 @@ def test_validate_files_action_not_found_invalid_domain( file_name = tmp_path / f"{file_type}.yml" file_name.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" {file_type}: - {data_type}: test path steps: @@ -183,7 +184,7 @@ def test_validate_files_form_not_found_invalid_domain( file_name = tmp_path / f"{file_type}.yml" file_name.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" {file_type}: - {data_type}: test path steps: @@ -229,8 +230,8 @@ def test_validate_files_with_active_loop_null(tmp_path: Path): def test_validate_files_form_slots_not_matching(tmp_path: Path): domain_file_name = tmp_path / "domain.yml" domain_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" forms: name_form: required_slots: @@ -290,7 +291,7 @@ def test_validate_files_invalid_slot_mappings(tmp_path: Path): slot_name = "started_booking_form" domain.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - activate_booking entities: diff --git a/tests/cli/test_rasa_test.py b/tests/cli/test_rasa_test.py index 0f8476f404d2..947297701389 100644 --- a/tests/cli/test_rasa_test.py +++ b/tests/cli/test_rasa_test.py @@ -47,7 +47,7 @@ def test_test_core_warnings(run_in_simple_project_with_model: Callable[..., RunR ) simple_test_story_yaml = """ -version: "3.0" +version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: unlikely path steps: diff --git a/tests/core/actions/test_forms.py b/tests/core/actions/test_forms.py index 335e7fd2fede..ee8714d15d90 100644 --- a/tests/core/actions/test_forms.py +++ b/tests/core/actions/test_forms.py @@ -10,7 +10,11 @@ from rasa.core.policies.policy import PolicyPrediction from rasa.core.actions import action from rasa.core.actions.action import ActionExecutionRejection, ActionExtractSlots -from rasa.shared.constants import REQUIRED_SLOTS_KEY, IGNORED_INTENTS +from rasa.shared.constants import ( + LATEST_TRAINING_DATA_FORMAT_VERSION, + REQUIRED_SLOTS_KEY, + IGNORED_INTENTS, +) from rasa.shared.core.constants import ACTION_LISTEN_NAME, REQUESTED_SLOT from rasa.core.actions.forms import FormAction from rasa.core.channels import CollectingOutputChannel @@ -119,7 +123,7 @@ async def test_switch_forms_with_same_slot(default_agent: Agent): utter_ask_form_2 = f"Please provide the value for {slot_a} of form 2" domain = f""" -version: "3.0" +version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" nlu: - intent: order_status examples: | @@ -448,7 +452,7 @@ async def test_validate_slots( tracker = DialogueStateTracker.from_events(sender_id="bla", evts=events) domain = f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - num_tables @@ -722,7 +726,7 @@ def test_temporary_tracker(): sender_id = "test" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: {extra_slot}: type: any @@ -1407,8 +1411,8 @@ async def test_extract_other_slots_with_matched_mapping_conditions(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intent: - greet - inform @@ -1479,8 +1483,8 @@ async def test_extract_other_slots_raises_no_matched_conditions(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intent: - greet - inform @@ -1549,8 +1553,8 @@ async def test_extract_other_slots_raises_no_matched_conditions(): async def test_action_extract_slots_custom_mapping_with_condition(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: custom_slot: @@ -1613,8 +1617,8 @@ async def test_action_extract_slots_custom_mapping_with_condition(): async def test_form_slots_empty_with_restart(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intent: - greet - inform diff --git a/tests/core/actions/test_two_stage_fallback.py b/tests/core/actions/test_two_stage_fallback.py index d98604891d75..4b916fd5b0d9 100644 --- a/tests/core/actions/test_two_stage_fallback.py +++ b/tests/core/actions/test_two_stage_fallback.py @@ -4,7 +4,10 @@ from rasa.core.policies.policy import PolicyPrediction from rasa.core.processor import MessageProcessor -from rasa.shared.constants import DEFAULT_NLU_FALLBACK_INTENT_NAME +from rasa.shared.constants import ( + DEFAULT_NLU_FALLBACK_INTENT_NAME, + LATEST_TRAINING_DATA_FORMAT_VERSION, +) from rasa.core.actions.two_stage_fallback import TwoStageFallbackAction from rasa.core.channels import CollectingOutputChannel from rasa.shared.core.domain import Domain @@ -156,7 +159,7 @@ async def test_ask_rephrase_after_failed_affirmation(): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_ask_rephrase: - text: {rephrase_text} diff --git a/tests/core/nlg/test_response.py b/tests/core/nlg/test_response.py index 2952f75dbbca..7db9984dd710 100644 --- a/tests/core/nlg/test_response.py +++ b/tests/core/nlg/test_response.py @@ -5,6 +5,7 @@ from _pytest.logging import LogCaptureFixture from rasa.core.nlg.response import TemplatedNaturalLanguageGenerator +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.domain import Domain from rasa.shared.core.slots import TextSlot, AnySlot, CategoricalSlot, BooleanSlot from rasa.shared.core.trackers import DialogueStateTracker @@ -250,7 +251,7 @@ async def test_nlg_conditional_response_variations_with_diff_slot_types( async def test_nlg_non_matching_channel(): domain = Domain.from_yaml( """ - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_hi: - text: "Hello" @@ -266,8 +267,8 @@ async def test_nlg_non_matching_channel(): async def test_nlg_conditional_response_variations_with_none_slot(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "text A" @@ -288,8 +289,8 @@ async def test_nlg_conditional_response_variations_with_none_slot(): async def test_nlg_conditional_response_variations_with_slot_not_a_constraint(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "text A" @@ -310,8 +311,8 @@ async def test_nlg_conditional_response_variations_with_slot_not_a_constraint(): async def test_nlg_conditional_response_variations_with_null_slot(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "text for null" @@ -336,8 +337,8 @@ async def test_nlg_conditional_response_variations_with_null_slot(): async def test_nlg_conditional_response_variations_channel_no_condition_met(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "example with channel" @@ -357,8 +358,8 @@ async def test_nlg_conditional_response_variations_channel_no_condition_met(): async def test_nlg_conditional_response_variation_condition_met_channel_mismatch(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "example with channel" @@ -423,8 +424,8 @@ async def test_nlg_conditional_response_variation_condition_met_channel_mismatch ) async def test_nlg_conditional_edgecases(slots, channel, expected_response): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "condition example A with channel" @@ -466,8 +467,8 @@ async def test_nlg_conditional_response_variations_condition_logging( caplog: LogCaptureFixture, ): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_action: - text: "example" diff --git a/tests/core/policies/test_rule_policy.py b/tests/core/policies/test_rule_policy.py index 5d075642d4ed..51f93f0f02af 100644 --- a/tests/core/policies/test_rule_policy.py +++ b/tests/core/policies/test_rule_policy.py @@ -7,7 +7,10 @@ from rasa.engine.graph import ExecutionContext from rasa.engine.storage.resource import Resource from rasa.engine.storage.storage import ModelStorage -from rasa.shared.constants import DEFAULT_NLU_FALLBACK_INTENT_NAME +from rasa.shared.constants import ( + DEFAULT_NLU_FALLBACK_INTENT_NAME, + LATEST_TRAINING_DATA_FORMAT_VERSION, +) from rasa.core import training from rasa.core.actions.action import ActionDefaultFallback @@ -149,7 +152,7 @@ def test_potential_contradiction_resolved_by_conversation_start(policy: RulePoli utter_anti_greet_action = "utter_anti_greet" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -202,7 +205,7 @@ def test_potential_contradiction_resolved_by_conversation_start_when_slot_initia some_slot_initial_value = "slot1value" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -275,7 +278,7 @@ def test_potential_contradiction_resolved_by_conversation_start_when_slot_initia some_slot_initial_value = "slot1value" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -340,7 +343,7 @@ def test_potential_contradiction_resolved_by_conversation_start_when_slot_initia def test_restrict_multiple_user_inputs_in_rules(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -372,7 +375,7 @@ def test_incomplete_rules_due_to_slots(policy: RulePolicy): some_slot = "some_slot" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -442,7 +445,7 @@ def test_no_incomplete_rules_due_to_slots_after_listen(policy: RulePolicy): some_slot = "some_slot" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -504,7 +507,7 @@ def test_no_incomplete_rules_due_to_additional_slots_set(policy: RulePolicy): some_other_slot_value = "value2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -559,7 +562,7 @@ def test_incomplete_rules_due_to_loops(policy: RulePolicy): some_form = "some_form" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} forms: @@ -624,7 +627,7 @@ def test_contradicting_rules(policy: RulePolicy): utter_anti_greet_action = "utter_anti_greet" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -664,7 +667,7 @@ def test_contradicting_rules_and_stories(policy: RulePolicy): utter_anti_greet_action = "utter_anti_greet" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -808,7 +811,7 @@ def test_rule_policy_contradicting_rule_finetune( def test_faq_rule(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -835,7 +838,7 @@ async def test_predict_form_action_if_in_form(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -880,7 +883,7 @@ async def test_predict_loop_action_if_in_loop_but_there_is_e2e_rule(policy: Rule domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -936,7 +939,7 @@ async def test_predict_form_action_if_multiple_turns(policy: RulePolicy): other_intent = "bye" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {other_intent} @@ -1085,7 +1088,7 @@ async def test_predict_action_listen_after_form(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1132,7 +1135,7 @@ async def test_dont_predict_form_if_already_finished(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1183,7 +1186,7 @@ async def test_form_unhappy_path(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1228,7 +1231,7 @@ async def test_form_unhappy_path_from_general_rule(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1285,7 +1288,7 @@ async def test_form_unhappy_path_from_in_form_rule(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1361,7 +1364,7 @@ async def test_form_unhappy_path_from_story(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1438,7 +1441,7 @@ async def test_form_unhappy_path_no_validation_from_rule( domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1529,7 +1532,7 @@ async def test_form_unhappy_path_no_validation_from_story(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1599,7 +1602,7 @@ async def test_form_unhappy_path_without_rule(policy: RulePolicy): other_intent = "bye" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {other_intent} @@ -1645,7 +1648,7 @@ async def test_form_activation_rule(policy: RulePolicy): other_intent = "bye" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {other_intent} @@ -1686,7 +1689,7 @@ async def test_failing_form_activation_due_to_no_rule(policy: RulePolicy): other_intent = "bye" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {other_intent} @@ -1727,7 +1730,7 @@ def test_form_submit_rule(policy: RulePolicy): submit_action_name = "utter_submit" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1781,7 +1784,7 @@ def test_immediate_submit(policy: RulePolicy): slot = "some_slot" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1887,7 +1890,7 @@ async def test_rule_policy_slot_filling_from_text( async def test_one_stage_fallback_rule(policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {DEFAULT_NLU_FALLBACK_INTENT_NAME} @@ -1970,7 +1973,7 @@ def test_default_actions( ): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -1998,7 +2001,7 @@ def test_default_actions( def test_e2e_beats_default_actions(intent_name: Text, policy: RulePolicy): domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} actions: @@ -2059,7 +2062,7 @@ def test_predict_core_fallback( other_intent = "other" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {other_intent} @@ -2092,7 +2095,7 @@ def test_predict_nothing_if_fallback_disabled( other_intent = "other" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {other_intent} @@ -2119,7 +2122,7 @@ def test_hide_rule_turn(policy: RulePolicy): action_chitchat = "action_chitchat" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {chitchat} @@ -2199,7 +2202,7 @@ def test_hide_rule_turn_with_slots( some_other_slot_value = "value2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {some_intent} - {some_other_intent} @@ -2326,7 +2329,7 @@ def test_hide_rule_turn_no_last_action_listen( followup_on_chitchat = "followup_on_chitchat" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {chitchat} actions: @@ -2414,7 +2417,7 @@ def test_hide_rule_turn_with_loops( action_chitchat = "action_chitchat" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {activate_form} @@ -2522,7 +2525,7 @@ def test_do_not_hide_rule_turn_with_loops_in_stories(policy: RulePolicy): activate_form = "activate_form" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {activate_form} slots: @@ -2577,7 +2580,7 @@ def test_hide_rule_turn_with_loops_as_followup_action(policy: RulePolicy): activate_form = "activate_form" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {GREET_INTENT_NAME} - {activate_form} @@ -2681,7 +2684,7 @@ def test_remove_action_listen_prediction_if_contradicts_with_story(policy: RuleP utter_2 = "utter_2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: @@ -2726,7 +2729,7 @@ def test_keep_action_listen_prediction_after_predictable_action(policy: RulePoli utter_3 = "utter_3" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: @@ -2772,7 +2775,7 @@ def test_keep_action_listen_prediction_if_last_prediction(policy: RulePolicy): utter_2 = "utter_2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: @@ -2813,7 +2816,7 @@ def test_keep_action_listen_prediction_if_contradicts_with_rule(policy: RulePoli utter_2 = "utter_2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: @@ -2856,7 +2859,7 @@ def test_raise_contradiction_if_rule_contradicts_with_story(policy: RulePolicy): utter_2 = "utter_2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: @@ -2898,7 +2901,7 @@ def test_rule_with_multiple_entities(policy: RulePolicy): utter_1 = "utter_1" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} entities: @@ -2962,7 +2965,7 @@ def test_rule_with_multiple_slots(policy: RulePolicy): slot_2 = "slot_2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: @@ -3030,7 +3033,7 @@ def test_include_action_unlikely_intent(policy: RulePolicy): slot_2 = "slot_2" domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {intent_1} actions: diff --git a/tests/core/policies/test_ted_policy.py b/tests/core/policies/test_ted_policy.py index 93a10b6d6914..8131006ab8c3 100644 --- a/tests/core/policies/test_ted_policy.py +++ b/tests/core/policies/test_ted_policy.py @@ -55,7 +55,7 @@ from rasa.shared.nlu.constants import ACTION_NAME from rasa.utils.tensorflow import model_data_utils from tests.core.test_policies import PolicyTestCollection -from rasa.shared.constants import DEFAULT_SENDER_ID +from rasa.shared.constants import DEFAULT_SENDER_ID, LATEST_TRAINING_DATA_FORMAT_VERSION UTTER_GREET_ACTION = "utter_greet" GREET_INTENT_NAME = "greet" @@ -226,8 +226,8 @@ def test_training_with_no_intent( ): stories = tmp_path / "stories.yml" stories.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: test path steps: diff --git a/tests/core/policies/test_unexpected_intent_policy.py b/tests/core/policies/test_unexpected_intent_policy.py index 07796d2d8daa..ec598015b048 100644 --- a/tests/core/policies/test_unexpected_intent_policy.py +++ b/tests/core/policies/test_unexpected_intent_policy.py @@ -14,6 +14,7 @@ from rasa.core.featurizers.tracker_featurizers import TrackerFeaturizer from rasa.core.featurizers.tracker_featurizers import IntentMaxHistoryTrackerFeaturizer from rasa.nlu.classifiers import LABEL_RANKING_LENGTH +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.generator import TrackerWithCachedStates from rasa.core.policies.ted_policy import PREDICTION_FEATURES from rasa.core.policies.unexpected_intent_policy import UnexpecTEDIntentPolicy @@ -138,8 +139,8 @@ def test_training_with_no_intent( ): stories = tmp_path / "stories.yml" stories.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: test path steps: diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index f5832dca7810..29f634fe08d1 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -27,7 +27,11 @@ from rasa.core.actions.forms import FormAction from rasa.core.channels import CollectingOutputChannel, OutputChannel from rasa.core.nlg import NaturalLanguageGenerator -from rasa.shared.constants import UTTER_PREFIX, REQUIRED_SLOTS_KEY +from rasa.shared.constants import ( + LATEST_TRAINING_DATA_FORMAT_VERSION, + UTTER_PREFIX, + REQUIRED_SLOTS_KEY, +) from rasa.shared.core.domain import ( ActionNotFoundException, SessionConfig, @@ -1146,8 +1150,8 @@ async def test_action_extract_slots_predefined_mappings( ): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - inform - greet @@ -1229,8 +1233,8 @@ async def test_action_extract_slots_predefined_mappings( async def test_action_extract_slots_with_from_trigger_mappings(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - inform @@ -1350,7 +1354,7 @@ async def test_action_extract_slots_with_list_slot( domain = Domain.from_yaml( textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - topping @@ -1449,7 +1453,7 @@ async def test_action_extract_slots_with_matched_mapping_condition(): domain = Domain.from_yaml( textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intent: - greet - inform @@ -1502,7 +1506,7 @@ async def test_action_extract_slots_no_matched_mapping_conditions(): domain = Domain.from_yaml( textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intent: - greet - inform @@ -1746,7 +1750,7 @@ async def test_extract_other_list_slot_from_entity( domain = Domain.from_yaml( textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - topping @@ -1965,8 +1969,8 @@ async def test_action_extract_slots_execute_validation_action( expected_events: List[Event], ): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet @@ -2029,8 +2033,8 @@ async def test_action_extract_slots_execute_validation_action( async def test_action_extract_slots_custom_action_and_predefined_slot_validation(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - inform @@ -2096,8 +2100,8 @@ async def test_action_extract_slots_custom_action_and_predefined_slot_validation async def test_action_extract_slots_with_duplicate_custom_actions(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - inform @@ -2168,8 +2172,8 @@ async def test_action_extract_slots_with_duplicate_custom_actions(): async def test_action_extract_slots_disallowed_events(caplog: LogCaptureFixture): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: custom_slot_one: @@ -2232,8 +2236,8 @@ async def test_action_extract_slots_warns_custom_action_exceptions( caplog: LogCaptureFixture, exception: Exception ): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: custom_slot_one: @@ -2278,8 +2282,8 @@ async def test_action_extract_slots_warns_custom_action_exceptions( async def test_action_extract_slots_with_empty_conditions(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - city @@ -2312,8 +2316,8 @@ async def test_action_extract_slots_with_empty_conditions(): async def test_action_extract_slots_with_not_existing_entity(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - city @@ -2350,8 +2354,8 @@ async def test_action_extract_slots_with_not_existing_entity(): async def test_action_extract_slots_with_not_existing_intent(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet @@ -2389,8 +2393,8 @@ async def test_action_extract_slots_with_not_existing_intent(): async def test_action_extract_slots_with_none_value_predefined_mapping(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - some_entity @@ -2430,8 +2434,8 @@ async def test_action_extract_slots_with_none_value_predefined_mapping(): async def test_action_extract_slots_with_none_value_custom_mapping(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: custom_slot: @@ -2473,8 +2477,8 @@ async def test_action_extract_slots_with_none_value_custom_mapping(): async def test_action_extract_slots_returns_bot_uttered(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: custom_slot: @@ -2521,8 +2525,8 @@ async def test_action_extract_slots_does_not_raise_disallowed_warning_for_slot_e caplog: LogCaptureFixture, ): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: custom_slot_a: @@ -2590,8 +2594,8 @@ async def test_action_extract_slots_does_not_raise_disallowed_warning_for_slot_e async def test_action_extract_slots_non_required_form_slot_with_from_entity_mapping(): domain_yaml = textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - form_start diff --git a/tests/core/test_evaluation.py b/tests/core/test_evaluation.py index 4c624d66c542..a56068a84514 100644 --- a/tests/core/test_evaluation.py +++ b/tests/core/test_evaluation.py @@ -184,7 +184,7 @@ async def test_end_to_evaluation_trips_circuit_breaker( ): config = textwrap.dedent( f""" - version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" policies: - name: MemoizationPolicy max_history: 11 diff --git a/tests/core/test_migrate.py b/tests/core/test_migrate.py index a241a4f55d21..cc68983bc774 100644 --- a/tests/core/test_migrate.py +++ b/tests/core/test_migrate.py @@ -12,6 +12,7 @@ from rasa.core import migrate from rasa.shared.core.domain import Domain from rasa.shared.exceptions import RasaException +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION def prepare_domain_path(directory: Path, domain_content: Text, file_name: Text) -> Path: @@ -80,7 +81,7 @@ def test_migrate_domain_format_with_required_slots( migrated_domain = rasa.shared.utils.io.read_yaml_file(domain_out_file) migrated_training_data_version = migrated_domain.get("version") - assert migrated_training_data_version == '"3.0"' + assert migrated_training_data_version == LATEST_TRAINING_DATA_FORMAT_VERSION migrated_slots = migrated_domain.get("slots") expected_slots = { @@ -348,7 +349,7 @@ def test_migrate_domain_format_from_dir(tmp_path: Path): migrated_file = rasa.shared.utils.io.read_yaml_file(file) migrated_training_data_version = migrated_file.get("version") - assert migrated_training_data_version == '"3.0"' + assert migrated_training_data_version == LATEST_TRAINING_DATA_FORMAT_VERSION def test_migrate_domain_all_keys(tmp_path: Path, domain_out_file: Path): @@ -396,7 +397,7 @@ def test_migrate_domain_all_keys(tmp_path: Path, domain_out_file: Path): assert "action_check_time" in migrated_actions migrated_training_data_version = migrated_domain.get("version") - assert migrated_training_data_version == '"3.0"' + assert migrated_training_data_version == LATEST_TRAINING_DATA_FORMAT_VERSION def test_migrate_domain_format_with_custom_slot(tmp_path: Path, domain_out_file: Path): @@ -767,7 +768,7 @@ def test_migrate_domain_from_dir_with_other_sections(tmp_path: Path): migrated = rasa.shared.utils.io.read_yaml_file(file) migrated_training_data_version = migrated.get("version") - assert migrated_training_data_version == '"3.0"' + assert migrated_training_data_version == LATEST_TRAINING_DATA_FORMAT_VERSION if file.name == domain_file_one: assert migrated.get("entities") == ["outdoor"] diff --git a/tests/core/test_processor.py b/tests/core/test_processor.py index 309acd749e8c..008527c31800 100644 --- a/tests/core/test_processor.py +++ b/tests/core/test_processor.py @@ -1247,7 +1247,7 @@ async def test_predict_next_action_with_hidden_rules( story_slot = "story_slot" domain_content = textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - {rule_intent} - {story_intent} diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 571837b991c0..8f9aaaa23a87 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -248,8 +248,8 @@ async def test_action_unlikely_intent_warning( file_name = tmp_path / "test_action_unlikely_intent_1.yml" file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: unlikely path steps: @@ -297,8 +297,8 @@ async def test_action_unlikely_intent_correctly_predicted( file_name = tmp_path / "test_action_unlikely_intent_2.yml" file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: unlikely path (with action_unlikely_intent) steps: @@ -342,8 +342,8 @@ async def test_wrong_action_after_action_unlikely_intent( test_file_name = tmp_path / "test.yml" test_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -360,8 +360,8 @@ async def test_wrong_action_after_action_unlikely_intent( train_file_name = tmp_path / "train.yml" train_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -411,8 +411,8 @@ async def test_action_unlikely_intent_not_found( ): test_file_name = tmp_path / "test_action_unlikely_intent_complete.yml" test_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -430,8 +430,8 @@ async def test_action_unlikely_intent_not_found( train_file_name = tmp_path / "train_without_action_unlikely_intent.yml" train_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -478,8 +478,8 @@ async def test_action_unlikely_intent_warning_and_story_error( test_file_name = tmp_path / "test.yml" test_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -496,8 +496,8 @@ async def test_action_unlikely_intent_warning_and_story_error( train_file_name = tmp_path / "train.yml" train_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -545,8 +545,8 @@ async def test_fail_on_prediction_errors( file_name = tmp_path / "test_action_unlikely_intent_2.yml" file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: unlikely path (with action_unlikely_intent) steps: diff --git a/tests/core/training/test_interactive.py b/tests/core/training/test_interactive.py index ad25591b81b8..e2e9624976e9 100644 --- a/tests/core/training/test_interactive.py +++ b/tests/core/training/test_interactive.py @@ -21,6 +21,7 @@ INTENT_MESSAGE_PREFIX, DEFAULT_SENDER_ID, DOCS_URL_POLICIES, + LATEST_TRAINING_DATA_FORMAT_VERSION, ) from rasa.shared.core.constants import ACTION_LISTEN_NAME, ACTION_UNLIKELY_INTENT_NAME from rasa.shared.core.domain import Domain @@ -573,7 +574,7 @@ async def test_write_domain_to_file_with_form(tmp_path: Path): form_name = "my_form" old_domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" actions: - utter_greet - utter_goodbye diff --git a/tests/nlu/extractors/test_extractor.py b/tests/nlu/extractors/test_extractor.py index edbba25ec3b3..d128f761064a 100644 --- a/tests/nlu/extractors/test_extractor.py +++ b/tests/nlu/extractors/test_extractor.py @@ -1,6 +1,7 @@ from typing import Any, Text, Dict, List import pytest +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.nlu.constants import TEXT, SPLIT_ENTITIES_BY_COMMA from rasa.shared.nlu.training_data.message import Message @@ -419,7 +420,7 @@ def test_split_entities_by_comma( "text, warnings", [ ( - 'version: "3.0"\n' + f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"\n' "nlu:\n" "- intent: test\n" " examples: |\n" @@ -427,7 +428,7 @@ def test_split_entities_by_comma( 1, ), ( - 'version: "3.0"\n' + f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"\n' "nlu:\n" "- intent: test\n" " examples: |\n" @@ -435,7 +436,7 @@ def test_split_entities_by_comma( 1, ), ( - 'version: "3.0"\n' + f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"\n' "nlu:\n" "- intent: test\n" " examples: |\n" @@ -444,7 +445,7 @@ def test_split_entities_by_comma( 1, ), ( - 'version: "3.0"\n' + f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"\n' "nlu:\n" "- intent: test\n" " examples: |\n" @@ -453,7 +454,7 @@ def test_split_entities_by_comma( 1, ), ( - 'version: "3.0"\n' + f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"\n' "nlu:\n" "- intent: test\n" " examples: |\n" diff --git a/tests/shared/core/test_domain.py b/tests/shared/core/test_domain.py index fb8f8777869b..8bc6deeb8dd2 100644 --- a/tests/shared/core/test_domain.py +++ b/tests/shared/core/test_domain.py @@ -178,8 +178,8 @@ def test_domain_from_template(domain: Domain): def test_avoid_action_repetition(domain: Domain): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" actions: - utter_greet responses: @@ -494,8 +494,8 @@ def test_merge_session_config_if_first_is_not_default(): def test_merge_with_empty_domain(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" config: store_entities_as_slots: false session_config: @@ -526,8 +526,8 @@ def test_merge_with_empty_domain(): @pytest.mark.parametrize("other", [Domain.empty(), None]) def test_merge_with_empty_other_domain(other: Optional[Domain]): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" config: store_entities_as_slots: false session_config: @@ -1171,7 +1171,7 @@ def test_not_add_knowledge_base_slots(): def test_add_knowledge_base_slots(): test_domain = Domain.from_yaml( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" actions: - {DEFAULT_KNOWLEDGE_BASE_ACTION} """ @@ -1488,8 +1488,8 @@ def test_form_invalid_mappings(domain_as_dict: Dict[Text, Any]): def test_form_invalid_required_slots_raises(): with pytest.raises(YamlValidationException): Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - some_entity forms: @@ -1557,23 +1557,23 @@ def test_slot_invalid_mappings(domain_as_dict: Dict[Text, Any]): [ # Wrong type for slots ( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: [] """ ), # Wrong type for slot names ( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: some_slot: 5 """ ), ( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: some_slot: [] """ @@ -1586,7 +1586,7 @@ def test_invalid_slots_raises_yaml_exception(domain_yaml: Text): def test_slot_order_is_preserved(): - test_yaml = f"""version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' + test_yaml = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" session_config: session_expiration_time: 60 carry_over_slots_to_new_session: true @@ -1675,7 +1675,7 @@ def test_slot_order_is_preserved_when_merging(): slots:{slot_2} """ - test_yaml_merged = f"""version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' + test_yaml_merged = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" session_config: session_expiration_time: 60 carry_over_slots_to_new_session: true @@ -1690,7 +1690,7 @@ def test_slot_order_is_preserved_when_merging(): def test_responses_text_multiline_is_preserved(): - test_yaml = f"""version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' + test_yaml = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" session_config: session_expiration_time: 60 carry_over_slots_to_new_session: true @@ -1792,8 +1792,8 @@ def test_domain_count_conditional_response_variations(): def test_domain_with_no_form_slots(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" forms: contract_form: required_slots: [] @@ -1805,8 +1805,8 @@ def test_domain_with_no_form_slots(): def test_domain_with_empty_required_slots(): with pytest.raises(YamlException): Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" forms: contract_form: """ @@ -1881,8 +1881,8 @@ def test_domain_duplicates_when_one_domain_file(): def test_domain_fingerprint_consistency_across_runs(): - domain_yaml = """ - version: "3.0" + domain_yaml = f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - goodbye @@ -1914,8 +1914,8 @@ def test_domain_fingerprint_consistency_across_runs(): def test_domain_fingerprint_uniqueness(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - goodbye @@ -1926,8 +1926,8 @@ def test_domain_fingerprint_uniqueness(): f1 = domain.fingerprint() domain_with_extra_intent = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - goodbye @@ -1940,8 +1940,8 @@ def test_domain_fingerprint_uniqueness(): assert f1 != f2 domain_with_extra_action = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - goodbye @@ -1954,8 +1954,8 @@ def test_domain_fingerprint_uniqueness(): assert f1 != f3 domain_with_extra_responses = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - goodbye @@ -1973,8 +1973,8 @@ def test_domain_fingerprint_uniqueness(): def test_domain_slots_for_entities_with_mapping_conditions_no_slot_set(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - city slots: @@ -2000,8 +2000,8 @@ def test_domain_slots_for_entities_with_mapping_conditions_no_slot_set(): def test_domain_slots_for_entities_sets_valid_slot(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - city slots: @@ -2021,8 +2021,8 @@ def test_domain_slots_for_entities_sets_valid_slot(): def test_domain_slots_for_entities_sets_valid_list_slot(): domain = Domain.from_yaml( textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - topping slots: @@ -2046,8 +2046,8 @@ def test_domain_slots_for_entities_sets_valid_list_slot(): def test_domain_slots_for_entities_with_entity_mapping_to_multiple_slots(): domain = Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - city slots: diff --git a/tests/shared/core/test_slot_mappings.py b/tests/shared/core/test_slot_mappings.py index 6ecd3d4db1cd..4b344e9524de 100644 --- a/tests/shared/core/test_slot_mappings.py +++ b/tests/shared/core/test_slot_mappings.py @@ -1,6 +1,7 @@ from typing import Text import pytest +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.domain import Domain from rasa.shared.core.events import UserUttered, ActiveLoop @@ -68,7 +69,7 @@ def test_slot_mapping_intent_is_desired(domain: Domain): def test_slot_mappings_ignored_intents_during_active_loop(): domain = Domain.from_yaml( """ - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet - chitchat @@ -104,8 +105,8 @@ def test_slot_mappings_ignored_intents_during_active_loop(): def test_missing_slot_mappings_raises(): with pytest.raises(YamlValidationException): Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" slots: some_slot: type: text @@ -117,8 +118,8 @@ def test_missing_slot_mappings_raises(): def test_slot_mappings_invalid_type_raises(): with pytest.raises(YamlValidationException): Domain.from_yaml( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - from_entity slots: diff --git a/tests/shared/core/test_trackers.py b/tests/shared/core/test_trackers.py index 7562bd7e9a7b..905756dd5f02 100644 --- a/tests/shared/core/test_trackers.py +++ b/tests/shared/core/test_trackers.py @@ -25,7 +25,7 @@ REQUESTED_SLOT, LOOP_INTERRUPTED, ) -from rasa.shared.constants import DEFAULT_SENDER_ID +from rasa.shared.constants import DEFAULT_SENDER_ID, LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.core.agent import Agent from rasa.shared.core.domain import Domain from rasa.shared.core.events import ( @@ -1378,7 +1378,7 @@ async def test_fill_slots_for_policy_entities(): domain = Domain.from_yaml( textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - {nlu_entity} - {policy_entity} diff --git a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py index 4c6f44ccd835..9f5cda7a0ab4 100644 --- a/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py +++ b/tests/shared/core/training_data/story_writer/test_yaml_story_writer.py @@ -3,6 +3,7 @@ from typing import Text from collections import OrderedDict import pytest +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.constants import ( ACTION_SESSION_START_NAME, @@ -87,8 +88,8 @@ def test_yaml_writer_dumps_user_messages(): assert ( dump.strip() == textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: default steps: @@ -115,8 +116,8 @@ def test_yaml_writer_doesnt_dump_action_unlikely_intent(): assert ( dump.strip() == textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: default steps: @@ -139,8 +140,8 @@ def test_yaml_writer_avoids_dumping_not_existing_user_messages(): assert ( dump.strip() == textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: default steps: @@ -198,7 +199,7 @@ def test_yaml_writer_stories_to_yaml_with_null_entities(domain: Domain): writer = YAMLStoryWriter() stories = textwrap.dedent( """ - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: happy path steps: @@ -251,7 +252,7 @@ def test_writing_end_to_end_stories(domain: Domain): dump.strip() == textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: {story_name} steps: @@ -298,7 +299,7 @@ def test_reading_and_writing_end_to_end_stories_in_test_mode(domain: Domain): dump.strip() == textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: {story_name} steps: diff --git a/tests/shared/core/training_data/test_structures.py b/tests/shared/core/training_data/test_structures.py index e17f5d413577..f42dcfe316ea 100644 --- a/tests/shared/core/training_data/test_structures.py +++ b/tests/shared/core/training_data/test_structures.py @@ -1,4 +1,5 @@ import rasa.core +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.constants import ACTION_SESSION_START_NAME from rasa.shared.core.domain import Domain from rasa.shared.core.events import ( @@ -41,7 +42,7 @@ def test_session_start_is_not_serialised(domain: Domain): Story.from_events(tracker.events, "some-story01").story_steps ) - expected = """version: "3.0" + expected = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-story01 steps: diff --git a/tests/shared/utils/test_validation.py b/tests/shared/utils/test_validation.py index a13b2acf7f3e..e32d55e364a6 100644 --- a/tests/shared/utils/test_validation.py +++ b/tests/shared/utils/test_validation.py @@ -50,8 +50,8 @@ def test_validate_yaml_schema_raise_exception(file: Text, schema: Text): def test_validate_yaml_schema_raise_exception_null_text(): - domain = """ - version: "3.0" + domain = f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_ask_email: - text: What is your email ID? @@ -68,8 +68,8 @@ def test_validate_yaml_schema_raise_exception_null_text(): def test_validate_yaml_schema_raise_exception_extra_hyphen_for_image(): - domain = """ - version: "3.0" + domain = f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" responses: utter_cheer_up: - image: https://i.imgur.com/nGF1K8f.jpg @@ -225,8 +225,8 @@ def test_concurrent_schema_validation(): successful_results = [] def validate() -> None: - payload = """ -version: "3.0" + payload = f""" +version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" nlu: - intent: greet examples: | diff --git a/tests/test_model_testing.py b/tests/test_model_testing.py index d030355372a9..a0e42bb08ac8 100644 --- a/tests/test_model_testing.py +++ b/tests/test_model_testing.py @@ -32,6 +32,7 @@ ENTITY_ATTRIBUTE_TYPE, ENTITY_ATTRIBUTE_TEXT, ) +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION def monkeypatch_get_latest_model(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: @@ -382,8 +383,8 @@ def test_write_classification_errors(): assert ( dump.strip() == textwrap.dedent( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: default steps: diff --git a/tests/test_model_training.py b/tests/test_model_training.py index 7f758279170b..3afc6d23da41 100644 --- a/tests/test_model_training.py +++ b/tests/test_model_training.py @@ -33,6 +33,7 @@ from rasa.nlu.classifiers.diet_classifier import DIETClassifier +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION import rasa.shared.utils.io from rasa.shared.core.domain import Domain from rasa.shared.exceptions import InvalidConfigException @@ -963,7 +964,7 @@ def test_invalid_graph_schema( ): config = textwrap.dedent( """ - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" recipe: "default.v1" pipeline: @@ -1003,7 +1004,7 @@ def test_fingerprint_changes_if_module_changes( config = textwrap.dedent( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" recipe: "default.v1" policies: diff --git a/tests/test_server.py b/tests/test_server.py index f4e658096d96..aaedc74a5ca4 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -70,6 +70,7 @@ ENTITY_ATTRIBUTE_VALUE, PREDICTED_CONFIDENCE_KEY, ) +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.model_training import TrainingResult from rasa.utils.endpoints import EndpointConfig from tests.conftest import AsyncMock, with_model_id, with_model_ids @@ -576,8 +577,8 @@ def assert_trained_model( async def test_train_with_yaml( rasa_app: SanicASGITestClient, tmp_path_factory: TempPathFactory ): - training_data = """ -version: "3.0" + training_data = f""" +version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: My story @@ -1811,7 +1812,7 @@ class NoInputChannels: ], None, True, - """version: "3.0" + f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID steps: @@ -1834,7 +1835,7 @@ class NoInputChannels: ], None, True, - """version: "3.0" + f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID, story 1 steps: @@ -1864,7 +1865,7 @@ class NoInputChannels: ], None, False, - """version: "3.0" + f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID steps: @@ -1888,7 +1889,7 @@ class NoInputChannels: ], None, None, - """version: "3.0" + f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID steps: @@ -1911,7 +1912,7 @@ class NoInputChannels: ], 4, True, - """version: "3.0" + f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID steps: @@ -1921,7 +1922,7 @@ class NoInputChannels: - action: utter_greet""", ), # empty conversation - ([], None, True, 'version: "3.0"'), + ([], None, True, f'version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}"'), # Conversation with slot ( [ @@ -1933,7 +1934,7 @@ class NoInputChannels: ], None, True, - """version: "3.0" + f"""version: "{rasa.shared.constants.LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID steps: @@ -2037,7 +2038,7 @@ async def test_get_story_does_not_update_conversation_session( # expected story is returned assert ( response.content.decode().strip() - == """version: "3.0" + == f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: some-conversation-ID steps: diff --git a/tests/test_validator.py b/tests/test_validator.py index adc41604989c..80d57ffb4e53 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -2,6 +2,7 @@ import pytest from _pytest.logging import LogCaptureFixture +from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.validator import Validator @@ -111,8 +112,8 @@ def test_verify_bad_story_structure(): def test_verify_bad_e2e_story_structure_when_text_identical(tmp_path: Path): story_file_name = tmp_path / "stories.yml" story_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: path 1 steps: @@ -310,8 +311,8 @@ def test_verify_there_is_not_example_repetition_in_intents(): def test_verify_actions_in_stories_not_in_domain(tmp_path: Path, domain_path: Text): story_file_name = tmp_path / "stories.yml" story_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" stories: - story: story path 1 steps: @@ -337,8 +338,8 @@ def test_verify_actions_in_stories_not_in_domain(tmp_path: Path, domain_path: Te def test_verify_actions_in_rules_not_in_domain(tmp_path: Path, domain_path: Text): rules_file_name = tmp_path / "rules.yml" rules_file_name.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" rules: - rule: rule path 1 steps: @@ -435,8 +436,8 @@ def test_verify_domain_with_duplicates( def test_verify_form_slots_invalid_domain(tmp_path: Path): domain = tmp_path / "domain.yml" domain.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" forms: name_form: required_slots: @@ -494,8 +495,8 @@ def test_valid_stories_rules_actions_in_domain( ): domain = tmp_path / "domain.yml" domain.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet actions: @@ -505,7 +506,7 @@ def test_valid_stories_rules_actions_in_domain( file_name = tmp_path / f"{file_name}.yml" file_name.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" {file_name}: - {data_type}: test path steps: @@ -526,8 +527,8 @@ def test_valid_stories_rules_default_actions( ): domain = tmp_path / "domain.yml" domain.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - greet """ @@ -535,7 +536,7 @@ def test_valid_stories_rules_default_actions( file_name = tmp_path / f"{file_name}.yml" file_name.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" {file_name}: - {data_type}: test path steps: @@ -551,8 +552,8 @@ def test_valid_stories_rules_default_actions( def test_valid_form_slots_in_domain(tmp_path: Path): domain = tmp_path / "domain.yml" domain.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" forms: name_form: required_slots: @@ -579,7 +580,7 @@ def test_verify_slot_mappings_mapping_active_loop_not_in_forms(tmp_path: Path): slot_name = "some_slot" domain.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" entities: - some_entity slots: @@ -615,7 +616,7 @@ def test_verify_slot_mappings_from_trigger_intent_mapping_slot_not_in_forms( slot_name = "started_booking_form" domain.write_text( f""" - version: "3.0" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - activate_booking entities: @@ -652,8 +653,8 @@ def test_verify_slot_mappings_from_trigger_intent_mapping_slot_not_in_forms( def test_verify_slot_mappings_slot_with_mapping_conditions_not_in_form(tmp_path: Path): domain = tmp_path / "domain.yml" domain.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - activate_booking entities: @@ -693,8 +694,8 @@ def test_verify_slot_mappings_slot_with_mapping_conditions_not_in_form(tmp_path: def test_verify_slot_mappings_valid(tmp_path: Path): domain = tmp_path / "domain.yml" domain.write_text( - """ - version: "3.0" + f""" + version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" intents: - activate_booking entities: From aaeb911f974837faea3c7f15667f99411ffc88e4 Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Tue, 22 Feb 2022 12:43:17 +0100 Subject: [PATCH 20/65] Sunset Segment/Metabase for regression tests (#10914) Changes: - Deprecate the Metabase regression test page (the page will stay online in case we need to inspect historical data, but not receive any new data) - Simplify the regression test implementation --- .github/scripts/mr_publish_results.py | 50 +------------------ .../ci-model-regression-on-schedule.yml | 3 +- .github/workflows/ci-model-regression.yml | 6 +-- 3 files changed, 4 insertions(+), 55 deletions(-) diff --git a/.github/scripts/mr_publish_results.py b/.github/scripts/mr_publish_results.py index 580aad24f104..374c9a2f3cc8 100644 --- a/.github/scripts/mr_publish_results.py +++ b/.github/scripts/mr_publish_results.py @@ -1,4 +1,4 @@ -# Send model regression test results to Segment and Datadog +# Send model regression test results to Datadog # with a summary of all test results. # Also write them into a report file. import copy @@ -7,7 +7,6 @@ import os from typing import Any, Dict, List, Text, Tuple -import analytics from datadog_api_client.v1 import ApiClient, Configuration from datadog_api_client.v1.api.metrics_api import MetricsApi from datadog_api_client.v1.model.metrics_payload import MetricsPayload @@ -28,14 +27,6 @@ "story_report.json": "story_prediction", } -TASK_MAPPING_SEGMENT = { - "intent_report.json": "Intent Classification", - "CRFEntityExtractor_report.json": "Entity Prediction", - "DIETClassifier_report.json": "Entity Prediction", - "response_selection_report.json": "Response Selection", - "story_report.json": "Story Prediction", -} - METRICS = { "test_run_time": "TEST_RUN_TIME", "train_run_time": "TRAIN_RUN_TIME", @@ -226,27 +217,6 @@ def send_to_datadog(results: List[Dict[Text, Any]]) -> None: print(response) -def _send_to_segment(context: Dict[Text, Any]) -> None: - jobID = os.environ["GITHUB_RUN_ID"] - analytics.identify( - jobID, {"name": "model-regression-tests", "created_at": datetime.datetime.now()} - ) - - analytics.track( - jobID, - "results", - { - "config_repository": CONFIG_REPOSITORY, - **prepare_datasetrepo_and_external_tags(), - **create_dict_of_env(METRICS), - **create_dict_of_env(MAIN_TAGS), - **create_dict_of_env(OTHER_TAGS), - **create_dict_of_env(GIT_RELATED_TAGS), - **context, - }, - ) - - def read_results(file: Text) -> Dict[Text, Any]: with open(file) as json_file: data = json.load(json_file) @@ -270,12 +240,6 @@ def get_result(file_name: Text, file: Text) -> Dict[Text, Any]: return result -def _push_results_to_segment(file_name: Text, file: Text) -> None: - result = get_result(file_name, file) - result["task"] = TASK_MAPPING_SEGMENT[file_name] - _send_to_segment(result) - - def send_all_to_datadog() -> None: results = [] for dirpath, dirnames, files in os.walk(os.environ["RESULT_DIR"]): @@ -286,17 +250,6 @@ def send_all_to_datadog() -> None: send_to_datadog(results) -def send_all_results_to_segment() -> None: - analytics.write_key = os.environ["SEGMENT_TOKEN"] - for dirpath, dirnames, files in os.walk(os.environ["RESULT_DIR"]): - for f in files: - if any( - f.endswith(valid_name) for valid_name in TASK_MAPPING_SEGMENT.keys() - ): - _push_results_to_segment(f, os.path.join(dirpath, f)) - analytics.flush() - - def generate_json(file: Text, task: Text, data: dict) -> dict: config = os.environ["CONFIG"] dataset = os.environ["DATASET"] @@ -336,5 +289,4 @@ def create_report_file() -> None: if __name__ == "__main__": send_all_to_datadog() - send_all_results_to_segment() create_report_file() diff --git a/.github/workflows/ci-model-regression-on-schedule.yml b/.github/workflows/ci-model-regression-on-schedule.yml index 59cd55af13aa..75893e429fac 100644 --- a/.github/workflows/ci-model-regression-on-schedule.yml +++ b/.github/workflows/ci-model-regression-on-schedule.yml @@ -324,11 +324,10 @@ jobs: echo "::set-output name=total_run_time::$(gomplate -i '{{ $t := time.Parse time.RFC3339 (getenv "NOW_TRAIN") }}{{ (time.Since $t).Round (time.Second 1) }}')" fi - - name: Generate a JSON file with a report / Publish results to Segment + Datadog + - name: Generate a JSON file with a report / Publish results to Datadog if: steps.set_dataset_config_vars.outputs.is_dataset_exists == 'true' && steps.set_dataset_config_vars.outputs.is_config_exists == 'true' env: SUMMARY_FILE: "./report.json" - SEGMENT_TOKEN: ${{ secrets.SEGMENT_TOKEN }} DATASET_NAME: ${{ matrix.dataset }} RESULT_DIR: "${{ github.workspace }}/results" CONFIG: ${{ matrix.config }} diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index b2241c74e48b..2f1846d37c6d 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -423,11 +423,10 @@ jobs: echo "::set-output name=total_run_time::$(gomplate -i '{{ $t := time.Parse time.RFC3339 (getenv "NOW_TRAIN") }}{{ (time.Since $t).Round (time.Second 1) }}')" fi - - name: Generate a JSON file with a report / Publish results to Segment + Datadog + - name: Generate a JSON file with a report / Publish results to Datadog if: steps.set_dataset_config_vars.outputs.is_dataset_exists == 'true' && steps.set_dataset_config_vars.outputs.is_config_exists == 'true' env: SUMMARY_FILE: "./report.json" - SEGMENT_TOKEN: ${{ secrets.SEGMENT_TOKEN }} DATASET_NAME: ${{ matrix.dataset }} RESULT_DIR: "${{ github.workspace }}/results" CONFIG: ${{ matrix.config }} @@ -665,11 +664,10 @@ jobs: echo "::set-output name=total_run_time::$(gomplate -i '{{ $t := time.Parse time.RFC3339 (getenv "NOW_TRAIN") }}{{ (time.Since $t).Round (time.Second 1) }}')" fi - - name: Generate a JSON file with a report / Publish results to Segment + Datadog + - name: Generate a JSON file with a report / Publish results to Datadog if: steps.set_dataset_config_vars.outputs.is_dataset_exists == 'true' && steps.set_dataset_config_vars.outputs.is_config_exists == 'true' env: SUMMARY_FILE: "./report.json" - SEGMENT_TOKEN: ${{ secrets.SEGMENT_TOKEN }} DATASET_NAME: ${{ matrix.dataset }} RESULT_DIR: "${{ github.workspace }}/results" CONFIG: ${{ matrix.config }} From 8c07aa21668bed4b0cd7c04d255a1c118ee2c80a Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Tue, 22 Feb 2022 12:46:49 +0100 Subject: [PATCH 21/65] Regr Tests: Add GPU NVML Interval (#10906) Changes: - Add `NVML_INTERVAL_IN_SEC` param to `start_dd_agent.sh` - The `NVML_INTERVAL_IN_SEC` param can be set as a global env variable in regression tests --- .github/scripts/start_dd_agent.sh | 14 ++++++++++---- .../workflows/ci-model-regression-on-schedule.yml | 3 ++- .github/workflows/ci-model-regression.yml | 5 +++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/scripts/start_dd_agent.sh b/.github/scripts/start_dd_agent.sh index 8f572b9071e7..dc28092a72db 100755 --- a/.github/scripts/start_dd_agent.sh +++ b/.github/scripts/start_dd_agent.sh @@ -2,6 +2,7 @@ DD_API_KEY=$1 ACCELERATOR_TYPE=$2 +NVML_INTERVAL_IN_SEC=${3:-15} # 15 seconds are the default interval # Install Datadog system agent DD_AGENT_MAJOR_VERSION=7 DD_API_KEY=$DD_API_KEY DD_SITE="datadoghq.eu" bash -c "$(curl -L https://s3.amazonaws.com/dd-agent/scripts/install_script.sh)" @@ -41,10 +42,15 @@ sudo chmod 666 $DATADOG_YAML_PATH sudo mv /etc/datadog-agent/conf.d/system_core.d/conf.yaml.example /etc/datadog-agent/conf.d/system_core.d/conf.yaml if [[ "${ACCELERATOR_TYPE}" == "GPU" ]]; then -# Install and enable NVML integration -sudo datadog-agent integration --allow-root install -t datadog-nvml==1.0.1 -sudo -u dd-agent -H /opt/datadog-agent/embedded/bin/pip3 install grpcio pynvml -sudo mv /etc/datadog-agent/conf.d/nvml.d/conf.yaml.example /etc/datadog-agent/conf.d/nvml.d/conf.yaml + # Install and enable NVML integration + sudo datadog-agent integration --allow-root install -t datadog-nvml==1.0.1 + sudo -u dd-agent -H /opt/datadog-agent/embedded/bin/pip3 install grpcio pynvml + NVML_CONF_FPATH="/etc/datadog-agent/conf.d/nvml.d/conf.yaml" + sudo mv "${NVML_CONF_FPATH}.example" ${NVML_CONF_FPATH} + if [[ "${NVML_INTERVAL_IN_SEC}" != 15 ]]; then + # Append a line to the NVML config file + sudo echo " min_collection_interval: ${NVML_INTERVAL_IN_SEC}" | sudo tee -a ${NVML_CONF_FPATH} > /dev/null + fi fi # Apply changes diff --git a/.github/workflows/ci-model-regression-on-schedule.yml b/.github/workflows/ci-model-regression-on-schedule.yml index 75893e429fac..0df83a0b1073 100644 --- a/.github/workflows/ci-model-regression-on-schedule.yml +++ b/.github/workflows/ci-model-regression-on-schedule.yml @@ -12,6 +12,7 @@ env: TF_FORCE_GPU_ALLOW_GROWTH: true GITHUB_ISSUE_LABELS: '["type:bug :bug:", "tool:model-regression-tests"]' PERFORMANCE_DROP_THRESHOLD: -0.05 + NVML_INTERVAL_IN_SEC: 1 jobs: read_test_configuration: @@ -240,7 +241,7 @@ jobs: TYPE: "${{ matrix.type }}" DATASET_REPOSITORY_BRANCH: "main" run: | - .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" + .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" ${{ env.NVML_INTERVAL_IN_SEC }} - name: Set up Python 3.8 🐍 uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index 2f1846d37c6d..678c48877603 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -21,6 +21,7 @@ env: GCLOUD_VERSION: "318.0.0" DD_PROFILING_ENABLED: false TF_FORCE_GPU_ALLOW_GROWTH: true + NVML_INTERVAL_IN_SEC: 1 jobs: read_test_configuration: @@ -339,7 +340,7 @@ jobs: DATASET_REPOSITORY_BRANCH: ${{ needs.read_test_configuration.outputs.dataset_branch }} run: | export PR_URL="https://github.com/${GITHUB_REPOSITORY}/pull/${{ github.event.number }}" - .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" + .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" ${{ env.NVML_INTERVAL_IN_SEC }} - name: Set up Python 3.8 🐍 uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a @@ -580,7 +581,7 @@ jobs: DATASET_REPOSITORY_BRANCH: ${{ matrix.dataset_branch }} run: | export PR_URL="https://github.com/${GITHUB_REPOSITORY}/pull/${{ github.event.number }}" - .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" + .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" ${{ env.NVML_INTERVAL_IN_SEC }} - name: Set up Python 3.8 🐍 uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a From f77206fba263c9043d811b3fb3a302d6dc4b7a64 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 13:09:32 +0100 Subject: [PATCH 22/65] fix abstract method in importer --- rasa/shared/importers/importer.py | 3 +-- rasa/shared/importers/rasa.py | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/rasa/shared/importers/importer.py b/rasa/shared/importers/importer.py index 95b9bbc2732e..7e4e4a3a0ef7 100644 --- a/rasa/shared/importers/importer.py +++ b/rasa/shared/importers/importer.py @@ -55,10 +55,9 @@ def get_config(self) -> Dict: """ raise NotImplementedError() - @rasa.shared.utils.common.cached_method def get_config_file_for_auto_config(self) -> Optional[Text]: """Returns config file path for auto-config only if there is a single one.""" - return self.config_file + raise NotImplementedError() def get_nlu_data(self, language: Optional[Text] = "en") -> TrainingData: """Retrieves the NLU training data that should be used for training. diff --git a/rasa/shared/importers/rasa.py b/rasa/shared/importers/rasa.py index 2426cfb9cd89..53ba35b5ee80 100644 --- a/rasa/shared/importers/rasa.py +++ b/rasa/shared/importers/rasa.py @@ -3,7 +3,8 @@ from typing import Dict, List, Optional, Text, Union import rasa.shared.data - +import rasa.shared.utils.common +import rasa.shared.utils.io from rasa.shared.core.training_data.structures import StoryGraph from rasa.shared.importers import utils from rasa.shared.importers.importer import TrainingDataImporter @@ -12,7 +13,6 @@ from rasa.shared.core.training_data.story_reader.yaml_story_reader import ( YAMLStoryReader, ) -import rasa.shared.utils.io logger = logging.getLogger(__name__) @@ -50,6 +50,11 @@ def get_config(self) -> Dict: config = rasa.shared.utils.io.read_model_configuration(self.config_file) return config + @rasa.shared.utils.common.cached_method + def get_config_file_for_auto_config(self) -> Optional[Text]: + """Returns config file path for auto-config only if there is a single one.""" + return self.config_file + def get_stories(self, exclusion_percentage: Optional[int] = None) -> StoryGraph: """Retrieves training stories / rules (see parent class for full docstring).""" return utils.story_graph_from_paths( From 31d7a0e085c793881bed94d17789fcdf0454164a Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 13:09:51 +0100 Subject: [PATCH 23/65] ruamel.yaml.allow_duplicate_keys doesnt exist --- rasa/shared/utils/io.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rasa/shared/utils/io.py b/rasa/shared/utils/io.py index 05216751d06e..fd58fd774324 100644 --- a/rasa/shared/utils/io.py +++ b/rasa/shared/utils/io.py @@ -302,7 +302,6 @@ def construct_yaml_str(self: BaseConstructor, node: ScalarNode) -> Any: yaml.Loader.add_constructor("tag:yaml.org,2002:str", construct_yaml_str) yaml.SafeLoader.add_constructor("tag:yaml.org,2002:str", construct_yaml_str) - yaml.allow_duplicate_keys = False def replace_environment_variables() -> None: From 0824ee7e28f6375284f041295ff3b002ddcaa3fe Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Tue, 22 Feb 2022 13:35:13 +0100 Subject: [PATCH 24/65] Remove legacy code (#10894) --- .../model_regression_test_results_legacy.tmpl | 159 ------------------ .../ci-model-regression-on-schedule.yml | 7 - .github/workflows/ci-model-regression.yml | 6 - 3 files changed, 172 deletions(-) delete mode 100644 .github/templates/model_regression_test_results_legacy.tmpl diff --git a/.github/templates/model_regression_test_results_legacy.tmpl b/.github/templates/model_regression_test_results_legacy.tmpl deleted file mode 100644 index 9d0056c7d0e4..000000000000 --- a/.github/templates/model_regression_test_results_legacy.tmpl +++ /dev/null @@ -1,159 +0,0 @@ -{{- /* - -The template reads a file with a report (the report file is available -as an artifact in the model regression tests workflow) and returns -a markdown table with a summary of the tests. - -*/ -}} -{{- /* - -The print_result_nlu template returns data depends on available fields. - -*/ -}} -{{ define "print_result_nlu" -}} -{{- if and (has (index .branch "micro avg") "f1-score") (has (index .main "micro avg") "f1-score") -}} -{{ printf "%.4f" (index (index .branch "micro avg") "f1-score") }} ({{ printf "%.2f" ((index (index .main "micro avg") "f1-score") | math.Sub (index (index .branch "micro avg") "f1-score")) }}) -{{- else if and (has .branch "accuracy") (has .main "accuracy") -}} -{{ printf "%.4f" .branch.accuracy }} ({{ printf "%.2f" (.main.accuracy | math.Sub .branch.accuracy) }}) -{{- else if and (has .branch "accuracy") (has (index .main "micro avg") "f1-score") -}} -{{ printf "%.4f" .branch.accuracy }} ({{ printf "%.2f" ((index (index .main "micro avg") "f1-score") | math.Sub .branch.accuracy) }}) -{{- else if and (has (index .branch "micro avg") "f1-score") (has .main "accuracy") -}} -{{ printf "%.4f" (index (index .branch "micro avg") "f1-score") }} ({{ printf "%.2f" (.main.accuracy | math.Sub (index (index .branch "micro avg") "f1-score")) }}) -{{- else if (has .branch "accuracy") -}} -{{ printf "%.4f" .branch.accuracy }} (`no data`) -{{- else if has (index .branch "micro avg") "f1-score" -}} -{{ printf "%.4f" (index (index .branch "micro avg") "f1-score") }} (`no data`) -{{- else -}} -`no data` -{{- end -}} -{{- end -}} -{{- /* - -The print_result_core template returns data depends on available fields. - -*/ -}} -{{ define "print_result_core_micro_avg" -}} -{{- if and (has (index .branch "micro avg") "f1-score") (has (index .main "micro avg") "f1-score") -}} -{{ printf "%.4f" (index (index .branch "micro avg") "f1-score") }} ({{ printf "%.2f" ((index (index .main "micro avg") "f1-score") | math.Sub (index (index .branch "micro avg") "f1-score")) }}) -{{- else if and (has .branch "accuracy") (has .main "accuracy") -}} -{{ printf "%.4f" .branch.accuracy }} ({{ printf "%.2f" (.main.accuracy | math.Sub .branch.accuracy) }}) -{{- else if and (has .branch "accuracy") (has (index .main "micro avg") "f1-score") -}} -{{ printf "%.4f" .branch.accuracy }} ({{ printf "%.2f" ((index (index .main "micro avg") "f1-score") | math.Sub .branch.accuracy) }}) -{{- else if and (has (index .branch "micro avg") "f1-score") (has .main "accuracy") -}} -{{ printf "%.4f" (index (index .branch "micro avg") "f1-score") }} ({{ printf "%.2f" (.main.accuracy | math.Sub (index (index .branch "micro avg") "f1-score")) }}) -{{- else if (has .branch "accuracy") -}} -{{ printf "%.4f" .branch.accuracy }} (`no data`) -{{- else if has (index .branch "micro avg") "f1-score" -}} -{{ printf "%.4f" (index (index .branch "micro avg") "f1-score") }} (`no data`) -{{- else -}} -`no data` -{{- end -}} -{{- end -}} - -{{ define "print_result_core_conversation_accuracy" -}} -{{- if and (has (index .branch "conversation_accuracy") "accuracy") (has (index .main "conversation_accuracy") "accuracy") -}} -{{ printf "%.4f" (index (index .branch "conversation_accuracy") "accuracy") }} ({{ printf "%.2f" ((index (index .main "conversation_accuracy") "accuracy") | math.Sub (index (index .branch "conversation_accuracy") "accuracy")) }}) -{{- else if has (index .branch "conversation_accuracy") "accuracy" -}} -{{ printf "%.4f" (index (index .branch "conversation_accuracy") "accuracy") }} (`no data`) -{{- else -}} -`no data` -{{- end -}} -{{- end -}} - -{{ define "print_table_nlu" }} -{{- $available_types := (index .results_for_dataset | jsonpath `@..type`) -}} -{{- if isKind "string" $available_types }}{{- $available_types = (index .results_for_dataset | jsonpath `@..type` | slice) -}}{{- end -}} -{{- if has $available_types "nlu" -}} -| Configuration | Intent Classification Micro F1 | Entity Recognition Micro F1 | Response Selection Micro F1 | -|---------------|-----------------|-----------------|-------------------| -{{ range $config_name, $config_data_array := .results_for_dataset -}} -{{ range $config_data := $config_data_array }} -{{- if eq $config_data.type "nlu" -}} -| `{{ $config_name }}`
test: `{{ $config_data.test_run_time }}`, train: `{{ $config_data.train_run_time }}`, total: `{{ $config_data.total_run_time }}`| -{{- if has $config_data "intent_classification" -}} -{{- $intent_class_main := dict -}} -{{- if has $.results_for_dataset_main $config_name -}} -{{- $intent_class_main = (index $.results_for_dataset_main $config_name).intent_classification -}} -{{- end -}} -{{- $intent_class := $config_data.intent_classification -}} -{{ template "print_result_nlu" (dict "branch" $intent_class "main" $intent_class_main) }}| -{{- else -}} -`no data`| -{{- end -}} -{{- if has $config_data "entity_prediction" -}} -{{- $entity_class_main := dict -}} -{{- if has $.results_for_dataset_main $config_name -}} -{{- $entity_class_main = (index $.results_for_dataset_main $config_name).entity_prediction -}} -{{- end -}} -{{- $entity_class := $config_data.entity_prediction -}} -{{ template "print_result_nlu" (dict "branch" $entity_class "main" $entity_class_main) }}| -{{- else -}} -`no data`| -{{- end -}} -{{- if has $config_data "response_selection" -}} -{{- $response_class_main := dict -}} -{{- if has $.results_for_dataset_main $config_name -}} -{{- $response_class_main = (index $.results_for_dataset_main $config_name).response_selection -}} -{{- end -}} -{{- $response_class := $config_data.response_selection -}} -{{ template "print_result_nlu" (dict "branch" $response_class "main" $response_class_main) }}| -{{- else -}} -`no data`| -{{- end }} -{{end}} -{{- end}} -{{- end}} -{{- end -}} -{{- end -}} - -{{- define "print_table_core" -}} -{{- $available_types := (index .results_for_dataset | jsonpath `@..type`) -}} -{{- if isKind "string" $available_types }}{{- $available_types = (index .results_for_dataset | jsonpath `@..type` | slice) -}}{{- end -}} -{{- if has $available_types "core" -}} -| Dialog Policy Configuration | Action Level Micro Avg. F1 | Conversation Level Accuracy | Run Time Train | Run Time Test | -|---------------|-----------------|-----------------|-------------------|-------------------| -{{ range $config_name, $config_data_array := .results_for_dataset -}} -{{ range $config_data := $config_data_array }} -{{- if eq $config_data.type "core" -}} -| `{{ $config_name }}` | -{{- if has $config_data "story_prediction" -}} -{{- $story_prediction_main := dict -}} -{{- if has $.results_for_dataset_main $config_name -}} -{{- $story_prediction_main = (index $.results_for_dataset_main $config_name).story_prediction -}} -{{- end -}} -{{- $story_prediction := $config_data.story_prediction -}} -{{ template "print_result_core_micro_avg" (dict "branch" $story_prediction "main" $story_prediction_main) }}| -{{- else -}} -`no data`| -{{- end -}} -{{- if has $config_data "story_prediction" -}} -{{- $story_prediction_main := dict -}} -{{- if has $.results_for_dataset_main $config_name -}} -{{- $story_prediction_main = (index $.results_for_dataset_main $config_name).story_prediction -}} -{{- end -}} -{{- $story_prediction := index $config_data.story_prediction -}} -{{ template "print_result_core_conversation_accuracy" (dict "branch" $story_prediction "main" $story_prediction_main) }}| -{{- else -}} -`no data`| -{{- end -}} -`{{ $config_data.train_run_time }}`| `{{ $config_data.test_run_time }}`| -{{ end }} -{{- end}} -{{- end}} -{{- end -}} -{{- end -}} - -{{- $results_main := (datasource "results_main") -}} -{{ range $dataset, $results_for_dataset := (datasource "data")}} -{{ $results_for_dataset_main := (index $results_main $dataset) -}} -{{ $content_dicts := index $results_for_dataset (index (keys $results_for_dataset) 0) -}} -{{ $one_content_dict := index $content_dicts 0 -}} -{{- if ($one_content_dict).external_dataset_repository -}} -Dataset: `{{$dataset}}`, Dataset repository branch: `{{ ($one_content_dict).dataset_repository_branch }}` (external repository), commit: `{{ ($one_content_dict).dataset_commit }}` -Configuration repository branch: `{{ ($one_content_dict).config_repository_branch }}` -{{ else -}} -Dataset: `{{$dataset}}`, Dataset repository branch: `{{ ($one_content_dict).dataset_repository_branch }}`, commit: `{{ ($one_content_dict).dataset_commit }}` -{{ end -}} -{{ template "print_table_nlu" (dict "results_for_dataset" $results_for_dataset "results_for_dataset_main" $results_for_dataset_main) }} -{{ template "print_table_core" (dict "results_for_dataset" $results_for_dataset "results_for_dataset_main" $results_for_dataset_main) }} -{{- end }} \ No newline at end of file diff --git a/.github/workflows/ci-model-regression-on-schedule.yml b/.github/workflows/ci-model-regression-on-schedule.yml index 0df83a0b1073..5658456affdb 100644 --- a/.github/workflows/ci-model-regression-on-schedule.yml +++ b/.github/workflows/ci-model-regression-on-schedule.yml @@ -551,14 +551,7 @@ jobs: - name: Analyse Performance 🔍 id: performance run: | - set +e OUTPUT="$(gomplate -d data=report.json -d results_main=report_main.json -f .github/templates/model_regression_test_results.tmpl)" - if [ $? -ne 0 ]; then - echo "New template failed. Try with legacy template." - OUTPUT="$(gomplate -d data=report.json -d results_main=report_main.json -f .github/templates/model_regression_test_results_legacy.tmpl)" - fi - set -e - OUTPUT="${OUTPUT//$'\n'/'%0A'}" OUTPUT="${OUTPUT//$'\r'/'%0D'}" OUTPUT="$(echo $OUTPUT | sed 's|`|\\`|g')" diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index 678c48877603..233c04919d44 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -820,13 +820,7 @@ jobs: - name: Render a comment to add id: get_results run: | - set +e OUTPUT="$(gomplate -d data=report.json -d results_main=report_main.json -f .github/templates/model_regression_test_results.tmpl)" - if [ $? -ne 0 ]; then - echo "New template failed. Try with legacy template." - OUTPUT="$(gomplate -d data=report.json -d results_main=report_main.json -f .github/templates/model_regression_test_results_legacy.tmpl)" - fi - set -e OUTPUT="${OUTPUT//$'\n'/'%0A'}" OUTPUT="${OUTPUT//$'\r'/'%0D'}" echo "::set-output name=result::$OUTPUT" From 981961fdef24493c885d2a24afcd5eefe5b3df73 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 14:13:01 +0100 Subject: [PATCH 25/65] fix flake8 lint --- rasa/server.py | 4 +++- .../core/training_data/story_writer/yaml_story_writer.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/rasa/server.py b/rasa/server.py index 44b54e652ed3..184f14659231 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -1364,7 +1364,9 @@ async def unload_model(request: Request) -> HTTPResponse: async def get_domain(request: Request) -> HTTPResponse: """Get current domain in yaml or json format.""" # FIXME: this is a false positive mypy error after upgrading to 0.931 - accepts = request.headers.get("Accept", default=JSON_CONTENT_TYPE) # type: ignore[call-overload] + accepts = request.headers.get( # type: ignore[call-overload] + "Accept", default=JSON_CONTENT_TYPE + ) if accepts.endswith("json"): domain = app.ctx.agent.domain.as_dict() return response.json(domain) diff --git a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py index e04debaa443a..80d3aee8d37c 100644 --- a/rasa/shared/core/training_data/story_writer/yaml_story_writer.py +++ b/rasa/shared/core/training_data/story_writer/yaml_story_writer.py @@ -213,11 +213,12 @@ def process_user_utterance( for entity in user_utterance.entities: if "value" in entity: if hasattr(user_utterance, "inline_comment_for_entity"): - # FIXME: to fix this type issue, WronglyClassifiedUserUtterance needs to - # be imported but it's currently outside of `rasa.shared` - for predicted in user_utterance.predicted_entities: # type: ignore[attr-defined] + # FIXME: to fix this type issue, WronglyClassifiedUserUtterance + # needs to be imported but it's currently outside + # of `rasa.shared` + for predicted in user_utterance.predicted_entities: # type: ignore[attr-defined] # noqa: E501 if predicted["start"] == entity["start"]: - commented_entity = user_utterance.inline_comment_for_entity( # type: ignore[attr-defined] + commented_entity = user_utterance.inline_comment_for_entity( # type: ignore[attr-defined] # noqa: E501 predicted, entity ) if commented_entity: From a5b4ec36c2e35a10e8e210895b0ca4ac810b43a0 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 14:46:44 +0100 Subject: [PATCH 26/65] fix more [attr-defined] type error --- rasa/core/channels/console.py | 9 ++++----- rasa/shared/core/slot_mappings.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rasa/core/channels/console.py b/rasa/core/channels/console.py index 00ed6b2f7a4b..520a385b7a8e 100644 --- a/rasa/core/channels/console.py +++ b/rasa/core/channels/console.py @@ -3,13 +3,12 @@ import json import logging import os +from typing import Any, AsyncGenerator, Dict, List, Optional, Text import aiohttp import questionary from aiohttp import ClientTimeout from prompt_toolkit.styles import Style -from typing import Any, Generator -from typing import Text, Optional, Dict, List import rasa.shared.utils.cli import rasa.shared.utils.io @@ -130,7 +129,7 @@ async def _send_message_receive_stream( sender_id: Text, message: Text, request_timeout: Optional[int] = None, -) -> Generator[Dict[Text, Any], None, None]: +) -> AsyncGenerator[Dict[Text, Any], None]: payload = {"sender": sender_id, "message": message} url = f"{server_url}/webhooks/rest/webhook?stream=true&token={auth_token}" @@ -189,11 +188,11 @@ async def record_messages( break if use_response_stream: - bot_responses = _send_message_receive_stream( + bot_responses_stream = _send_message_receive_stream( server_url, auth_token, sender_id, text, request_timeout=request_timeout ) previous_response = None - async for response in bot_responses: + async for response in bot_responses_stream: if previous_response is not None: _print_bot_output(previous_response) previous_response = response diff --git a/rasa/shared/core/slot_mappings.py b/rasa/shared/core/slot_mappings.py index 94f3234434d3..8dfeaf724bf6 100644 --- a/rasa/shared/core/slot_mappings.py +++ b/rasa/shared/core/slot_mappings.py @@ -54,7 +54,7 @@ def validate(mapping: Dict[Text, Any], slot_name: Text) -> None: f"{DOCS_URL_SLOTS} for more information." ) - validations = { + validations: Dict[SlotMappingType, List[Text]] = { SlotMappingType.FROM_ENTITY: ["entity"], SlotMappingType.FROM_INTENT: ["value"], SlotMappingType.FROM_TRIGGER_INTENT: ["value"], From 991cd6a7aa9b133bc6ea4808afb25177d18ee955 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 15:03:24 +0100 Subject: [PATCH 27/65] update towncrier command to new API --- scripts/release.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index f23f06152d46..f215f4bd0138 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -285,7 +285,8 @@ def next_version(args: argparse.Namespace) -> Version: def generate_changelog(version: Version) -> None: """Call tonwcrier and create a changelog from all available changelog entries.""" check_call( - ["towncrier", "--yes", "--version", str(version)], cwd=str(project_root()) + ["towncrier", "build", "--yes", "--version", str(version)], + cwd=str(project_root()), ) From 89fddd24edd686dc33e2dbc086da5984dddc1a74 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 15:24:43 +0100 Subject: [PATCH 28/65] fix changelog template --- changelog/_template.md.jinja2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/_template.md.jinja2 b/changelog/_template.md.jinja2 index 1369ad3fba7a..a96ea9aacf48 100644 --- a/changelog/_template.md.jinja2 +++ b/changelog/_template.md.jinja2 @@ -1,5 +1,5 @@ {# Based on https://github.com/hawkowl/towncrier/blob/master/src/towncrier/templates/default.rst #} -{% for section in sections %}{% if section %}{{section}}{% endif %}{% if sections[section] %}{% for category, val in definitions.items() if category in sections[section] %} +{% if top_line %}{{ top_line }} {{ top_underline * ((top_line)|length)}} {% elif versiondata.name %}{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }}) {{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}{% else %}{{ versiondata.version }} ({{ versiondata.date }}) {{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}{% endif %}{% for section in sections %}{% if section %}{{section}}{% endif %}{% if sections[section] %}{% for category, val in definitions.items() if category in sections[section] %} {{ "### " + definitions[category]['name'] }} {% if definitions[category]['showcontent'] %}{% for text, values in sections[section][category]|dictsort(by='value') %}{% set issue_joiner = joiner(', ') %}- {% for value in values|sort %}{{ issue_joiner() }}{{ value }}{% endfor %}: {{ text }} @@ -7,4 +7,4 @@ {% else %}{% endif %}{% endfor %}{% else %} No significant changes. -{% endif %}{% endfor %} \ No newline at end of file +{% endif %}{% endfor %} From b175039425a06b8afacacea05e1a025e647241e0 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 16:00:52 +0100 Subject: [PATCH 29/65] fix black formatting --- rasa/core/actions/forms.py | 35 +++++++++++++++++++---------------- rasa/model_training.py | 9 +++++++-- rasa/server.py | 3 ++- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/rasa/core/actions/forms.py b/rasa/core/actions/forms.py index bcb4c8b44c54..0c68086ccc94 100644 --- a/rasa/core/actions/forms.py +++ b/rasa/core/actions/forms.py @@ -631,22 +631,25 @@ async def is_done( # We explicitly check only the last occurrences for each possible termination # event instead of doing `return event in events_so_far` to make it possible # to override termination events which were returned earlier. - return next( - ( - event - for event in reversed(events_so_far) - if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT - ), - None, - ) == SlotSet(REQUESTED_SLOT, None) or next( - ( - event - for event in reversed(events_so_far) - if isinstance(event, ActiveLoop) - ), - None, - ) == ActiveLoop( - None + return ( + next( + ( + event + for event in reversed(events_so_far) + if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT + ), + None, + ) + == SlotSet(REQUESTED_SLOT, None) + or next( + ( + event + for event in reversed(events_so_far) + if isinstance(event, ActiveLoop) + ), + None, + ) + == ActiveLoop(None) ) async def deactivate(self, *args: Any, **kwargs: Any) -> List[Event]: diff --git a/rasa/model_training.py b/rasa/model_training.py index 7b5ba9945168..ce7b0db600df 100644 --- a/rasa/model_training.py +++ b/rasa/model_training.py @@ -200,10 +200,15 @@ def _train_graph( config = file_importer.get_config() recipe = Recipe.recipe_for_name(config.get("recipe")) config, _missing_keys, _configured_keys = recipe.auto_configure( - file_importer.get_config_file_for_auto_config(), config, training_type, + file_importer.get_config_file_for_auto_config(), + config, + training_type, ) model_configuration = recipe.graph_config_for_recipe( - config, kwargs, training_type=training_type, is_finetuning=is_finetuning, + config, + kwargs, + training_type=training_type, + is_finetuning=is_finetuning, ) rasa.engine.validation.validate(model_configuration) diff --git a/rasa/server.py b/rasa/server.py index 184f14659231..b7e2aacf4707 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -80,7 +80,8 @@ response.HTTPResponse, Coroutine[Any, Any, response.HTTPResponse] ] SanicView = Callable[ - [Arg(Request, "request"), VarArg(), KwArg()], SanicResponse, # noqa: F821 + [Arg(Request, "request"), VarArg(), KwArg()], + SanicResponse, # noqa: F821 ] From 262db6454aa45b8884ef711fe5038236812f5cff Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 16:11:52 +0100 Subject: [PATCH 30/65] fix flake8 lint x2 --- rasa/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rasa/server.py b/rasa/server.py index b7e2aacf4707..20af856c7801 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -80,8 +80,8 @@ response.HTTPResponse, Coroutine[Any, Any, response.HTTPResponse] ] SanicView = Callable[ - [Arg(Request, "request"), VarArg(), KwArg()], - SanicResponse, # noqa: F821 + [Arg(Request, "request"), VarArg(), KwArg()], # noqa: F821 + SanicResponse, ] From 621bfd1e4318a52c790f0bea8e1dd3ac97e3bb1a Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 16:31:22 +0100 Subject: [PATCH 31/65] fix sanic.Blueprint signature --- stubs/sanic/blueprints.pyi | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 stubs/sanic/blueprints.pyi diff --git a/stubs/sanic/blueprints.pyi b/stubs/sanic/blueprints.pyi new file mode 100644 index 000000000000..7160108c3b6c --- /dev/null +++ b/stubs/sanic/blueprints.pyi @@ -0,0 +1,10 @@ +from typing import Any, Dict, Text + +from sanic.app import Sanic + +# mypy check fails here but it actually successfully loads the initial module +# so it's probably an internal issue of mypy with no repercussions +from sanic.blueprints import Blueprint as SanicBlueprint # type: ignore[attr-defined] + +class Blueprint(SanicBlueprint): + def register(self, app: Sanic, options: Dict[Text, Any]) -> None: ... From f68493724edf614bba891df4a41446bc30760e48 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 16:31:54 +0100 Subject: [PATCH 32/65] fix RedisTrackerStore.keys return type --- rasa/core/tracker_store.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rasa/core/tracker_store.py b/rasa/core/tracker_store.py index dcd2b364527e..54f3d06b4173 100644 --- a/rasa/core/tracker_store.py +++ b/rasa/core/tracker_store.py @@ -327,6 +327,7 @@ def __init__( ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile, ssl_ca_certs=ssl_ca_certs, + decode_responses=True, ) self.record_exp = record_exp From 0732661fcd32de2350f98d3a880aaaa1fc0236df Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 16:48:51 +0100 Subject: [PATCH 33/65] give type hint for QueueOutputChannel.messages --- rasa/core/channels/rest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rasa/core/channels/rest.py b/rasa/core/channels/rest.py index f70c5edb71a5..4765016a956b 100644 --- a/rasa/core/channels/rest.py +++ b/rasa/core/channels/rest.py @@ -145,6 +145,8 @@ class QueueOutputChannel(CollectingOutputChannel): (doesn't send them anywhere, just collects them).""" + messages: Queue + @classmethod def name(cls) -> Text: return "queue" From 85eebf5eccdd968284d2422f136813fcab3820a7 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 16:57:02 +0100 Subject: [PATCH 34/65] use TypeVar and TypeAlias for get_last_event_for() --- rasa/shared/core/trackers.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rasa/shared/core/trackers.py b/rasa/shared/core/trackers.py index 82bd2b4287b2..fcf102861a08 100644 --- a/rasa/shared/core/trackers.py +++ b/rasa/shared/core/trackers.py @@ -12,6 +12,7 @@ Iterator, Generator, Type, + TypeVar, List, Deque, Iterable, @@ -64,7 +65,7 @@ from rasa.shared.core.slots import Slot if TYPE_CHECKING: - from typing_extensions import TypedDict + from typing_extensions import TypedDict, TypeAlias from rasa.shared.core.events import NLUPredictionData from rasa.shared.core.training_data.structures import Story @@ -82,6 +83,8 @@ total=False, ) + EventType: TypeAlias = TypeVar("EventType", Event) + logger = logging.getLogger(__name__) @@ -717,11 +720,11 @@ def export_stories_to_file(self, export_path: Text = "debug_stories.yml") -> Non def get_last_event_for( self, - event_type: Union[Type[Event], Tuple[Type, ...]], + event_type: Union[Type[EventType], Tuple[Type[EventType], ...]], action_names_to_exclude: List[Text] = None, skip: int = 0, event_verbosity: EventVerbosity = EventVerbosity.APPLIED, - ) -> Optional[Event]: + ) -> Optional[EventType]: """Gets the last event of a given type which was actually applied. Args: From a1b25c00533df5720e172448ddbda84d547afa49 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 17:25:50 +0100 Subject: [PATCH 35/65] fix docstring lint --- rasa/core/channels/rest.py | 1 + rasa/nlu/persistor.py | 1 + rasa/shared/core/slots.py | 1 + rasa/shared/core/trackers.py | 1 - rasa/shared/core/training_data/visualization.py | 1 - 5 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rasa/core/channels/rest.py b/rasa/core/channels/rest.py index 4765016a956b..0cd8c04c0f7b 100644 --- a/rasa/core/channels/rest.py +++ b/rasa/core/channels/rest.py @@ -149,6 +149,7 @@ class QueueOutputChannel(CollectingOutputChannel): @classmethod def name(cls) -> Text: + """Name of QueueOutputChannel.""" return "queue" # noinspection PyMissingConstructor diff --git a/rasa/nlu/persistor.py b/rasa/nlu/persistor.py index f933d528e584..057c8da851f6 100644 --- a/rasa/nlu/persistor.py +++ b/rasa/nlu/persistor.py @@ -162,6 +162,7 @@ class GCSPersistor(Persistor): Fetches them when needed, instead of storing them on the local disk.""" def __init__(self, bucket_name: Text) -> None: + """Initialise class with client and bucket.""" # there are no type hints in this repo for now # https://github.com/googleapis/python-storage/issues/393 from google.cloud import storage # type: ignore[attr-defined] diff --git a/rasa/shared/core/slots.py b/rasa/shared/core/slots.py index b7b73bcf5e6d..d9889bda6b80 100644 --- a/rasa/shared/core/slots.py +++ b/rasa/shared/core/slots.py @@ -284,6 +284,7 @@ def _as_feature(self) -> List[float]: @property def value(self) -> Any: + """Gets the slot's value.""" return super().value @value.setter diff --git a/rasa/shared/core/trackers.py b/rasa/shared/core/trackers.py index fcf102861a08..23faad52b65a 100644 --- a/rasa/shared/core/trackers.py +++ b/rasa/shared/core/trackers.py @@ -738,7 +738,6 @@ def get_last_event_for( Returns: event which matched the query or `None` if no event matched. """ - to_exclude = action_names_to_exclude or [] def filter_function(e: Event) -> bool: diff --git a/rasa/shared/core/training_data/visualization.py b/rasa/shared/core/training_data/visualization.py index ae4f2fb2e79b..e22dc03e3ed4 100644 --- a/rasa/shared/core/training_data/visualization.py +++ b/rasa/shared/core/training_data/visualization.py @@ -324,7 +324,6 @@ def persist_graph(graph: "networkx.Graph", output_file: Text) -> None: def _length_of_common_action_prefix(this: List[Event], other: List[Event]) -> int: """Calculate number of actions that two conversations have in common.""" - num_common_actions = 0 t_cleaned = cast( List[Union[ActionExecuted, UserUttered]], From e26aad198e7b342daba3ca138570ee30b8fb57cc Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 17:31:53 +0100 Subject: [PATCH 36/65] fix undefined type annotation at runtime --- rasa/shared/core/trackers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rasa/shared/core/trackers.py b/rasa/shared/core/trackers.py index 23faad52b65a..5f4448371a7c 100644 --- a/rasa/shared/core/trackers.py +++ b/rasa/shared/core/trackers.py @@ -83,7 +83,7 @@ total=False, ) - EventType: TypeAlias = TypeVar("EventType", Event) + EventTypeAlias: TypeAlias = TypeVar("EventTypeAlias", Event) logger = logging.getLogger(__name__) @@ -720,11 +720,11 @@ def export_stories_to_file(self, export_path: Text = "debug_stories.yml") -> Non def get_last_event_for( self, - event_type: Union[Type[EventType], Tuple[Type[EventType], ...]], + event_type: Union[Type["EventTypeAlias"], Tuple[Type["EventTypeAlias"], ...]], action_names_to_exclude: List[Text] = None, skip: int = 0, event_verbosity: EventVerbosity = EventVerbosity.APPLIED, - ) -> Optional[EventType]: + ) -> Optional["EventTypeAlias"]: """Gets the last event of a given type which was actually applied. Args: From 3d21a8bfb9e895cec92cfdc683667b2bddf8838f Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 17:39:15 +0100 Subject: [PATCH 37/65] fix ListSlot.value setter (super() issue) --- rasa/shared/core/slots.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/rasa/shared/core/slots.py b/rasa/shared/core/slots.py index d9889bda6b80..495add19062a 100644 --- a/rasa/shared/core/slots.py +++ b/rasa/shared/core/slots.py @@ -282,12 +282,8 @@ def _as_feature(self) -> List[float]: # we couldn't convert the value to a list - using default value return [0.0] - @property - def value(self) -> Any: - """Gets the slot's value.""" - return super().value - - @value.setter + # FIXME: https://github.com/python/mypy/issues/8085 + @Slot.value.setter # type: ignore[attr-defined] def value(self, value: Any) -> None: """Sets the slot's value.""" if value and not isinstance(value, list): @@ -295,7 +291,8 @@ def value(self, value: Any) -> None: value = [value] # Call property setter of superclass - super().value.fset(self, value) + # FIXME: https://github.com/python/mypy/issues/8085 + super(ListSlot, self.__class__).value.fset(self, value) # type: ignore[attr-defined] # noqa: E501 class CategoricalSlot(Slot): From a2cb6b72bb72fb9e5598808d564749503ee08784 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 22 Feb 2022 18:19:48 +0100 Subject: [PATCH 38/65] fix transformers typing issues --- rasa/nlu/featurizers/dense_featurizer/lm_featurizer.py | 2 +- rasa/nlu/utils/hugging_face/registry.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rasa/nlu/featurizers/dense_featurizer/lm_featurizer.py b/rasa/nlu/featurizers/dense_featurizer/lm_featurizer.py index f5b5a3eb7b02..9c89c6ed87d5 100644 --- a/rasa/nlu/featurizers/dense_featurizer/lm_featurizer.py +++ b/rasa/nlu/featurizers/dense_featurizer/lm_featurizer.py @@ -150,7 +150,7 @@ def _load_model_instance(self) -> None: self.tokenizer = model_tokenizer_dict[self.model_name].from_pretrained( self.model_weights, cache_dir=self.cache_dir ) - self.model = model_class_dict[self.model_name].from_pretrained( + self.model = model_class_dict[self.model_name].from_pretrained( # type: ignore[no-untyped-call] # noqa: E501 self.model_weights, cache_dir=self.cache_dir ) diff --git a/rasa/nlu/utils/hugging_face/registry.py b/rasa/nlu/utils/hugging_face/registry.py index 002269c02226..f7f6cc4c4d02 100644 --- a/rasa/nlu/utils/hugging_face/registry.py +++ b/rasa/nlu/utils/hugging_face/registry.py @@ -1,10 +1,12 @@ import logging +from typing import Dict, Text, Type # Explicitly set logging level for this module before any import # because otherwise it logs tensorflow/pytorch versions logging.getLogger("transformers.file_utils").setLevel(logging.WARNING) from transformers import ( # noqa: F401, E402 + TFPreTrainedModel, TFBertModel, TFOpenAIGPTModel, TFGPT2Model, @@ -12,6 +14,7 @@ # TFXLMModel, TFDistilBertModel, TFRobertaModel, + PreTrainedTokenizer, BertTokenizer, OpenAIGPTTokenizer, GPT2Tokenizer, @@ -36,7 +39,7 @@ ) -model_class_dict = { +model_class_dict: Dict[Text, Type[TFPreTrainedModel]] = { "bert": TFBertModel, "gpt": TFOpenAIGPTModel, "gpt2": TFGPT2Model, @@ -46,7 +49,7 @@ "distilbert": TFDistilBertModel, "roberta": TFRobertaModel, } -model_tokenizer_dict = { +model_tokenizer_dict: Dict[Text, Type[PreTrainedTokenizer]] = { "bert": BertTokenizer, "gpt": OpenAIGPTTokenizer, "gpt2": GPT2Tokenizer, From fd58a98a39d338e5b2062e6c96034bacb743968a Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Wed, 23 Feb 2022 11:15:52 +0100 Subject: [PATCH 39/65] Regr test: Add link to datadog dashboard in result comment (#10915) Changes: - Add link to DD dashboard in PR result comment - Use commit time as start time - Use time of posting the comment (after the MR tests) as end time --- .github/workflows/ci-model-regression.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index 233c04919d44..74d63d1740e0 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -825,6 +825,17 @@ jobs: OUTPUT="${OUTPUT//$'\r'/'%0D'}" echo "::set-output name=result::$OUTPUT" + # Get time of current commit as start time + TIME_ISO_COMMIT=$(gomplate -d github=https://api.github.com/repos/rasaHQ/rasa/commits/${{ github.sha }} -H 'github=Authorization:token ${{ secrets.GITHUB_TOKEN }}' -i '{{ (ds "github").commit.author.date }}') # Example "2022-02-17T14:06:38Z" + TIME_UNIX_COMMIT=$(date -d "${TIME_ISO_COMMIT}" +%s%3N) # Example: "1645106798" + + # Get current time + TIME_ISO_NOW=$(gomplate -i '{{ (time.Now).UTC.Format time.RFC3339}}') # Example: "2022-02-17T14:50:54Z%" + TIME_UNIX_NOW=$(date -d "${TIME_ISO_NOW}" +%s%3N) # Example: "1645118083" + + echo "::set-output name=from_ts::$TIME_UNIX_COMMIT" + echo "::set-output name=to_ts::$TIME_UNIX_NOW" + - name: Publish results as a PR comment uses: marocchino/sticky-pull-request-comment@v2.2.0 if: ${{ always() }} @@ -837,6 +848,8 @@ jobs: Commit: ${{ github.sha }}, [The full report is available as an artifact.](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + [Datadog dashboard link](https://app.datadoghq.eu/dashboard/mf4-2hu-x84?tpl_var_branch_baseline=${{ github.head_ref }}&from_ts=${{ steps.get_results.outputs.from_ts }}&to_ts=${{ steps.get_results.outputs.to_ts }}&live=false) + ${{ steps.get_results.outputs.result }} - name: Remove 'status:model-regression-tests' label From 9bb43b27dfe1feb33e3b223e8745dcf50fa50721 Mon Sep 17 00:00:00 2001 From: sanchariGr Date: Wed, 23 Feb 2022 16:05:35 +0100 Subject: [PATCH 40/65] 10823 update slack installation doc --- docs/docs/connectors/slack.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/docs/connectors/slack.mdx b/docs/docs/connectors/slack.mdx index 472b645e00c8..38541bae736e 100644 --- a/docs/docs/connectors/slack.mdx +++ b/docs/docs/connectors/slack.mdx @@ -11,6 +11,7 @@ import interactivityImg from './img/slack-interactivity.png'; import requestUrlImg from './img/slack-request-url.png'; import scopesImg from './img/slack-scopes.png'; import secretImg from './img/slack-secret.png'; +import appHomeImg from './img/slack-create-app.png'; Connecting a bot to Slack requires you to configure it to send messages (using API credentials) and to receive messages (using a webhook). @@ -110,6 +111,12 @@ your bot and tell you about new messages. If you are running locally, you can If you are running locally, make sure ngrok (or another tool to retrieve a public url) is running as well. +2. To send messages directly to your bot using the slack UI, head to **App Home**, + scroll to bottom and turn on checkbox for + `Allow users to send Slash commands and messages from the messages tab.` + + Allow users to send Slash commands and messages from the messages tab + 2. Configure the webhook by heading to **Event Subscriptions** and turning **Enable Events** on. From d0abce20c64062f4bf0a146dca0c03051d8e9cb2 Mon Sep 17 00:00:00 2001 From: sanchariGr Date: Wed, 23 Feb 2022 16:34:31 +0100 Subject: [PATCH 41/65] added missing png files --- docs/docs/connectors/img/slack-app-home.png | Bin 0 -> 321623 bytes docs/docs/connectors/slack.mdx | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/docs/connectors/img/slack-app-home.png diff --git a/docs/docs/connectors/img/slack-app-home.png b/docs/docs/connectors/img/slack-app-home.png new file mode 100644 index 0000000000000000000000000000000000000000..c42e39996b7bc2465adc3f63bb5a45fde3748e87 GIT binary patch literal 321623 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
    ~LCQMdFS*=%&L48&+a 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 uRRbvTnixKY{3>TUUjeNCKQ^BH3H -2. Configure the webhook by heading to **Event Subscriptions** and +3. Configure the webhook by heading to **Event Subscriptions** and turning **Enable Events** on. As a request URL enter the public url of your bot and append `/webhooks/slack/webhook`, e.g. @@ -128,7 +128,7 @@ your bot and tell you about new messages. If you are running locally, you can Request URL Screenshot -3. As a last step, you'll need to **Subscribe to bot events** on the same page. +4. As a last step, you'll need to **Subscribe to bot events** on the same page. You'll need to add the following events: - `message.channels`, - `message.groups`, From 7e477a511b2f1570f27feceed2fa8160e6114646 Mon Sep 17 00:00:00 2001 From: sanchariGr Date: Wed, 23 Feb 2022 16:47:12 +0100 Subject: [PATCH 42/65] add changelog --- changelog/10940.doc.md | 2 ++ docs/docs/connectors/slack.mdx | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 changelog/10940.doc.md diff --git a/changelog/10940.doc.md b/changelog/10940.doc.md new file mode 100644 index 000000000000..f0a5e38ddbf6 --- /dev/null +++ b/changelog/10940.doc.md @@ -0,0 +1,2 @@ +Added an additional step to `Receiving Messages` section in slack.mdx documentation. After a slack update this +addiotional step is needed to allow direct messages to the bot. diff --git a/docs/docs/connectors/slack.mdx b/docs/docs/connectors/slack.mdx index 65a269bc8737..06d0dccb99ae 100644 --- a/docs/docs/connectors/slack.mdx +++ b/docs/docs/connectors/slack.mdx @@ -115,6 +115,8 @@ your bot and tell you about new messages. If you are running locally, you can scroll to bottom and turn on checkbox for `Allow users to send Slash commands and messages from the messages tab.` + You might have to quit the Slack app and re-open it before this takes affect + Allow users to send Slash commands and messages from the messages tab 3. Configure the webhook by heading to **Event Subscriptions** and From 348697b7e43d5aa478aa3dc7ccd3fc2140facebc Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Wed, 23 Feb 2022 19:04:50 +0100 Subject: [PATCH 43/65] Fix dataset name bug (#10935) Use dataset name (e.g. financial-demo) instead of dataset path (e.g. RasaHQ/financial-demo) --- .github/scripts/mr_publish_results.py | 4 ++-- .github/tests/test_mr_publish_results.py | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/scripts/mr_publish_results.py b/.github/scripts/mr_publish_results.py index 374c9a2f3cc8..da27a6bc0e18 100644 --- a/.github/scripts/mr_publish_results.py +++ b/.github/scripts/mr_publish_results.py @@ -35,7 +35,7 @@ MAIN_TAGS = { "config": "CONFIG", - "dataset": "DATASET", + "dataset": "DATASET_NAME", } OTHER_TAGS = { @@ -252,7 +252,7 @@ def send_all_to_datadog() -> None: def generate_json(file: Text, task: Text, data: dict) -> dict: config = os.environ["CONFIG"] - dataset = os.environ["DATASET"] + dataset = os.environ["DATASET_NAME"] if dataset not in data: data = {dataset: {config: []}, **data} diff --git a/.github/tests/test_mr_publish_results.py b/.github/tests/test_mr_publish_results.py index 4408a752e7fb..fcd7e253ab30 100644 --- a/.github/tests/test_mr_publish_results.py +++ b/.github/tests/test_mr_publish_results.py @@ -9,16 +9,24 @@ prepare_ml_metrics, transform_to_seconds, generate_json, + prepare_datadog_tags, ) EXAMPLE_CONFIG = "Sparse + BERT + DIET(seq) + ResponseSelector(t2t)" -EXAMPLE_DATASET = "financial-demo" +EXAMPLE_DATASET_NAME = "financial-demo" ENV_VARS = { + "BRANCH": "my-branch", + "PR_ID": "10927", + "PR_URL": "https://github.com/RasaHQ/rasa/pull/10856/", + "GITHUB_EVENT_NAME": "pull_request", + "GITHUB_RUN_ID": "1882718340", + "GITHUB_SHA": "abc", + "GITHUB_WORKFLOW": "CI - Model Regression", "IS_EXTERNAL": "false", "DATASET_REPOSITORY_BRANCH": "main", "CONFIG": EXAMPLE_CONFIG, - "DATASET": EXAMPLE_DATASET, + "DATASET_NAME": EXAMPLE_DATASET_NAME, "CONFIG_REPOSITORY_BRANCH": "main", "DATASET_COMMIT": "52a3ad3eb5292d56542687e23b06703431f15ead", "ACCELERATOR_TYPE": "CPU", @@ -34,9 +42,9 @@ def test_generate_json(): f = Path(__file__).parent / "test_data" / "intent_report.json" result = generate_json(f, task="intent_classification", data={}) - assert isinstance(result[EXAMPLE_DATASET][EXAMPLE_CONFIG], list) + assert isinstance(result[EXAMPLE_DATASET_NAME][EXAMPLE_CONFIG], list) - actual = result[EXAMPLE_DATASET][EXAMPLE_CONFIG][0]["intent_classification"] + actual = result[EXAMPLE_DATASET_NAME][EXAMPLE_CONFIG][0]["intent_classification"] expected = { "accuracy": 1.0, "weighted avg": { @@ -115,3 +123,9 @@ def test_prepare_ml_model_perf_metrics_simple(): key, value = "Intent Classification.weighted avg.f1-score", 1.0 assert key in metrics_ml and value == metrics_ml[key] + + +@mock.patch.dict(os.environ, ENV_VARS, clear=True) +def test_prepare_datadog_tags(): + tags_list = prepare_datadog_tags() + assert "dataset:financial-demo" in tags_list From 16b9e8005d879b3a9f6566bbd584fb14c9af01e5 Mon Sep 17 00:00:00 2001 From: Sanchari Date: Thu, 24 Feb 2022 09:10:24 +0100 Subject: [PATCH 44/65] Update changelog/10940.doc.md Co-authored-by: Melinda Loubser <32034278+melindaloubser1@users.noreply.github.com> --- changelog/10940.doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/10940.doc.md b/changelog/10940.doc.md index f0a5e38ddbf6..d8c4b5f788d8 100644 --- a/changelog/10940.doc.md +++ b/changelog/10940.doc.md @@ -1,2 +1,2 @@ Added an additional step to `Receiving Messages` section in slack.mdx documentation. After a slack update this -addiotional step is needed to allow direct messages to the bot. +additional step is needed to allow direct messages to the bot. From 416cf8f79bfc1e553cf1f91f10d854f3845b3ea0 Mon Sep 17 00:00:00 2001 From: Sanchari Date: Thu, 24 Feb 2022 09:11:14 +0100 Subject: [PATCH 45/65] Update docs/docs/connectors/slack.mdx Co-authored-by: Melinda Loubser <32034278+melindaloubser1@users.noreply.github.com> --- docs/docs/connectors/slack.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/connectors/slack.mdx b/docs/docs/connectors/slack.mdx index 06d0dccb99ae..646766a1285d 100644 --- a/docs/docs/connectors/slack.mdx +++ b/docs/docs/connectors/slack.mdx @@ -112,10 +112,10 @@ your bot and tell you about new messages. If you are running locally, you can url) is running as well. 2. To send messages directly to your bot using the slack UI, head to **App Home**, - scroll to bottom and turn on checkbox for + scroll to the bottom and select the checkbox for `Allow users to send Slash commands and messages from the messages tab.` - You might have to quit the Slack app and re-open it before this takes affect + You might have to quit the Slack app and re-open it before your changes take effect. Allow users to send Slash commands and messages from the messages tab From abc93271061f1c267ad026d28526ff9e576e8b7d Mon Sep 17 00:00:00 2001 From: m-vdb Date: Thu, 24 Feb 2022 09:54:13 +0100 Subject: [PATCH 46/65] cast to Policy in DefaultV1RecipeValidator --- rasa/graph_components/validators/default_recipe_validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rasa/graph_components/validators/default_recipe_validator.py b/rasa/graph_components/validators/default_recipe_validator.py index 0b5b02a2e5db..59613492defa 100644 --- a/rasa/graph_components/validators/default_recipe_validator.py +++ b/rasa/graph_components/validators/default_recipe_validator.py @@ -1,6 +1,6 @@ from __future__ import annotations from collections import defaultdict -from typing import Iterable, List, Dict, Text, Any, Set, Type +from typing import Iterable, List, Dict, Text, Any, Set, Type, cast from rasa.core.featurizers.precomputation import CoreFeaturizationInputConverter from rasa.engine.graph import ExecutionContext, GraphComponent, GraphSchema, SchemaNode @@ -477,7 +477,7 @@ def _warn_if_rule_based_data_is_unused_or_missing( story_graph: a story graph (core training data) """ consuming_rule_data = any( - policy_node.uses.supported_data() + cast(Policy, policy_node.uses).supported_data() in [SupportedData.RULE_DATA, SupportedData.ML_AND_RULE_DATA] for policy_node in self._policy_schema_nodes ) From 93d6f66781bf33da38bff2fa1085c2753d92f304 Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Thu, 24 Feb 2022 15:28:12 +0100 Subject: [PATCH 47/65] Regr Test: Fix missing comment (#10938) Changes: * Fix jq bug: Use select with == instead = --- .github/workflows/ci-model-regression-on-schedule.yml | 10 +++++----- .github/workflows/ci-model-regression.yml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-model-regression-on-schedule.yml b/.github/workflows/ci-model-regression-on-schedule.yml index 5658456affdb..290719903944 100644 --- a/.github/workflows/ci-model-regression-on-schedule.yml +++ b/.github/workflows/ci-model-regression-on-schedule.yml @@ -427,7 +427,7 @@ jobs: return issue.data.number - name: Notify Slack of Failure 😱 - if: failure() && steps.issue-exists.outputs.result == 'false' && github.event_name == 'schedule' + if: failure() && steps.issue-exists.outputs.result == 'false' && github.event_name == 'schedule' uses: 8398a7/action-slack@a74b761b4089b5d730d813fbedcd2ec5d394f3af # v3 with: status: custom @@ -524,16 +524,16 @@ jobs: # Get ID of last on-schedule workflow SCHEDULE_ID=$(curl -X GET -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/workflows" \ - | jq -r '.workflows[] | select(.name == "${{ github.workflow }}") | select(.path | test("schedule")) | .id') + | jq -r '.workflows[] | select(.name == "${{ github.workflow }}") | select(.path | test("schedule")) | .id') - ARTIFACT_URL=$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" \ + ARTIFACT_URL=$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/workflows/${SCHEDULE_ID}/runs?event=schedule&status=completed&branch=main&per_page=1" | jq -r .workflow_runs[0].artifacts_url) DOWNLOAD_URL=$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" "${ARTIFACT_URL}" \ - | jq -r '.artifacts[] | select(.name="report.json") | .archive_download_url') + | jq -r '.artifacts[] | select(.name == "report.json") | .archive_download_url') # Download the artifact - curl -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -LJO -H "Accept: application/vnd.github.v3+json" $DOWNLOAD_URL + curl -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -LJO -H "Accept: application/vnd.github.v3+json" $DOWNLOAD_URL # Unzip and change name unzip report.json.zip && mv report.json report_main.json diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index 74d63d1740e0..3a5b9b93928d 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -793,16 +793,16 @@ jobs: # Get ID of last on-schedule workflow SCHEDULE_ID=$(curl -X GET -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/workflows" \ - | jq -r '.workflows[] | select(.name == "CI - Model Regression on schedule") | select(.path | test("schedule")) | .id') + | jq -r '.workflows[] | select(.name == "CI - Model Regression on schedule") | select(.path | test("schedule")) | .id') - ARTIFACT_URL=$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" \ + ARTIFACT_URL=$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/workflows/${SCHEDULE_ID}/runs?event=schedule&status=completed&branch=main&per_page=1" | jq -r .workflow_runs[0].artifacts_url) DOWNLOAD_URL=$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -H "Accept: application/vnd.github.v3+json" "${ARTIFACT_URL}" \ - | jq -r '.artifacts[] | select(.name="report.json") | .archive_download_url') + | jq -r '.artifacts[] | select(.name == "report.json") | .archive_download_url') # Download the artifact - curl -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -LJO -H "Accept: application/vnd.github.v3+json" $DOWNLOAD_URL + curl -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' -LJO -H "Accept: application/vnd.github.v3+json" $DOWNLOAD_URL # Unzip and change name unzip report.json.zip && mv report.json report_main.json From e9e99c31fabaa04e9f93942cec93abc33bde5218 Mon Sep 17 00:00:00 2001 From: Anca Lita <27920906+ancalita@users.noreply.github.com> Date: Fri, 25 Feb 2022 16:44:18 +0000 Subject: [PATCH 48/65] Improve Domain Loading: Resolve Technical Debt (#10841) * move utility methods into shared utils package, extract shared merging functionality * refactor duplication checks * reduce complexity of utils merge function * unify domain merge method, amend tests, add original data repr as domain attribute * code quality fix * add arg description in docstring * adjust failing tests * fix code quality fails * fix unit test * rearrange utils methods and remove domain_loading.py * refactor Domain merge method as instance method * revert some of the test changes * update Domain data property * amend ResponsesSyncImporter * update data parameter in interactive.py * experiment with removing as_dict * change to domain.data in server.py * add some test fixes * more test fixes * add unit tests for common.py utility methods and add changelog * apply review suggestions * fix Domain bug * fix changed input during Domain.from_dict * remove transform_for_files Domain methods & update changelog * move constants, remove duplicate code and tidy up * fix import error + docstring linting * more import fixes * revert exception change, replace version constant * remove some default addition to self.data * remove cleaned_domain method * fix fingerprint method, reduce method complexity * reduce complexity of _reset_intent_flags method * fix test assertions because of more slot auto-fill UserWarnings * refactor if statement in merge method, limit slot validation only if slots are defined in the domain, amend tests * remove is_dir parameter and refactor if statement, fix unit test --- changelog/10807.misc.md | 6 + data/test_domains/duplicate_entities.yml | 2 - rasa/cli/data.py | 1 - .../converters/responses_prefix_converter.py | 4 +- rasa/core/training/interactive.py | 24 +- .../domain_for_core_training_provider.py | 4 +- rasa/shared/core/domain.py | 546 ++++++++---------- rasa/shared/importers/importer.py | 44 +- rasa/shared/importers/multi_project.py | 4 +- rasa/shared/utils/common.py | 70 +++ rasa/validator.py | 34 +- tests/core/evaluation/test_marker.py | 2 +- tests/core/featurizers/test_precomputation.py | 2 + .../test_single_state_featurizers.py | 4 + tests/core/test_actions.py | 1 + tests/core/test_processor.py | 1 + .../test_example_bots_training_data.py | 51 +- .../test_default_recipe_validator.py | 11 +- .../classifiers/test_regex_message_handler.py | 1 + tests/shared/core/test_domain.py | 198 +++---- .../story_reader/test_yaml_story_reader.py | 3 + tests/shared/utils/test_common.py | 84 +++ tests/test_model_training.py | 3 +- tests/test_server.py | 12 +- tests/test_validator.py | 106 +--- 25 files changed, 579 insertions(+), 639 deletions(-) create mode 100644 changelog/10807.misc.md diff --git a/changelog/10807.misc.md b/changelog/10807.misc.md new file mode 100644 index 000000000000..2d6f7e3c58c8 --- /dev/null +++ b/changelog/10807.misc.md @@ -0,0 +1,6 @@ +Domain loading was improved in certain areas: +- unified the different merging methods in order to use a single method in the case of both loading a list of paths and domain directory. +- extracted several general utility methods used in merging to `rasa/shared/utils/common.py` +- removed the `self.duplicates` attributes stored in the `Domain` instance and raised a warning instead when duplicates are found during merging +- removed the `Validator.verify_domain_duplicates` method making use of the `self.duplicates` removed attribute +- serialisation of `Domain` objects now uses the initial dictionary representation (rather than the re-transformed version used before); diff --git a/data/test_domains/duplicate_entities.yml b/data/test_domains/duplicate_entities.yml index 2199cefb1791..c3fcf1f7bfca 100644 --- a/data/test_domains/duplicate_entities.yml +++ b/data/test_domains/duplicate_entities.yml @@ -2,8 +2,6 @@ intents: - greet - default - goodbye - - default - - goodbye slots: cuisine: diff --git a/rasa/cli/data.py b/rasa/cli/data.py index 9c6d00b952ff..3b56cc620815 100644 --- a/rasa/cli/data.py +++ b/rasa/cli/data.py @@ -197,7 +197,6 @@ def validate_stories(args: argparse.Namespace) -> None: def _validate_domain(validator: "Validator") -> bool: return ( validator.verify_domain_validity() - and validator.verify_domain_duplicates() and validator.verify_actions_in_stories_rules() and validator.verify_forms_in_stories_rules() and validator.verify_form_slots() diff --git a/rasa/core/training/converters/responses_prefix_converter.py b/rasa/core/training/converters/responses_prefix_converter.py index f16edd2fb7f9..8be36e933831 100644 --- a/rasa/core/training/converters/responses_prefix_converter.py +++ b/rasa/core/training/converters/responses_prefix_converter.py @@ -109,7 +109,7 @@ async def convert_and_write(cls, source_path: Path, output_path: Path) -> None: output_path: Path to the output directory. """ domain = Domain.from_path(source_path) - domain_dict = domain.cleaned_domain() + domain_dict = domain.as_dict() domain_dict["actions"] = [ normalize_utter_action(action) for action in domain_dict["actions"] ] @@ -118,4 +118,4 @@ async def convert_and_write(cls, source_path: Path, output_path: Path) -> None: output_file = cls.generate_path_for_converted_training_data_file( source_path, output_path ) - new_domain.persist_clean(output_file) + new_domain.persist(output_file) diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py index 34a5a262ceeb..477a9cf4c84a 100644 --- a/rasa/core/training/interactive.py +++ b/rasa/core/training/interactive.py @@ -50,7 +50,13 @@ from rasa.core import run, utils import rasa.core.train from rasa.core.constants import DEFAULT_SERVER_FORMAT, DEFAULT_SERVER_PORT -from rasa.shared.core.domain import Domain +from rasa.shared.core.domain import ( + Domain, + KEY_INTENTS, + KEY_ENTITIES, + KEY_RESPONSES, + KEY_ACTIONS, +) import rasa.shared.core.events from rasa.shared.core.events import ( ActionExecuted, @@ -929,16 +935,16 @@ def _write_domain_to_file( } ) - new_domain = Domain( - intents=_intents_from_messages(messages), - entities=_entities_from_messages(messages), - slots=[], - responses=responses, - action_names=collected_actions, - forms={}, + new_domain = Domain.from_dict( + { + KEY_INTENTS: list(_intents_from_messages(messages)), + KEY_ENTITIES: _entities_from_messages(messages), + KEY_RESPONSES: responses, + KEY_ACTIONS: collected_actions, + } ) - old_domain.merge(new_domain).persist_clean(domain_path) + old_domain.merge(new_domain).persist(domain_path) async def _predict_till_next_listen( diff --git a/rasa/graph_components/providers/domain_for_core_training_provider.py b/rasa/graph_components/providers/domain_for_core_training_provider.py index ae1455de277f..e3dc17842e76 100644 --- a/rasa/graph_components/providers/domain_for_core_training_provider.py +++ b/rasa/graph_components/providers/domain_for_core_training_provider.py @@ -80,8 +80,8 @@ def create_pruned_version(domain: Domain) -> Domain: serialized_domain.pop("config", None) # `store_entities_as_slots` serialized_domain.pop(SESSION_CONFIG_KEY, None) - for response_name in serialized_domain[KEY_RESPONSES]: + for response_name in serialized_domain.get(KEY_RESPONSES, []): serialized_domain[KEY_RESPONSES][response_name] = [] - for form_name in serialized_domain[KEY_FORMS]: + for form_name in serialized_domain.get(KEY_FORMS, []): serialized_domain[KEY_FORMS][form_name] = {REQUIRED_SLOTS_KEY: []} return Domain.from_dict(serialized_domain) diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index 653596991634..c22785b31774 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -8,7 +8,6 @@ Any, Dict, List, - NamedTuple, NoReturn, Optional, Set, @@ -17,6 +16,8 @@ Union, TYPE_CHECKING, Iterable, + NamedTuple, + Callable, ) from ruamel.yaml.scalarstring import DoubleQuotedScalarString @@ -31,10 +32,15 @@ DOCS_URL_RESPONSES, REQUIRED_SLOTS_KEY, IGNORED_INTENTS, + RESPONSE_CONDITION, ) import rasa.shared.core.constants from rasa.shared.core.constants import SlotMappingType, MAPPING_TYPE, MAPPING_CONDITIONS -from rasa.shared.exceptions import RasaException, YamlException, YamlSyntaxException +from rasa.shared.exceptions import ( + RasaException, + YamlException, + YamlSyntaxException, +) import rasa.shared.utils.validation import rasa.shared.utils.io import rasa.shared.utils.common @@ -42,7 +48,6 @@ from rasa.shared.core.events import SlotSet, UserUttered from rasa.shared.core.slots import Slot, CategoricalSlot, TextSlot, AnySlot, ListSlot from rasa.shared.utils.validation import KEY_TRAINING_DATA_FORMAT_VERSION -from rasa.shared.constants import RESPONSE_CONDITION from rasa.shared.nlu.constants import ( ENTITY_ATTRIBUTE_TYPE, ENTITY_ATTRIBUTE_ROLE, @@ -123,19 +128,29 @@ def are_sessions_enabled(self) -> bool: """Returns a boolean value depending on the value of session_expiration_time.""" return self.session_expiration_time > 0 + def as_dict(self) -> Dict: + """Return serialized `SessionConfig`.""" + return { + "session_expiration_time": self.session_expiration_time, + "carry_over_slots_to_new_session": self.carry_over_slots, + } + class Domain: """The domain specifies the universe in which the bot's policy acts. A Domain subclass provides the actions the bot can take, the intents - and entities it can recognise.""" + and entities it can recognise. + """ @classmethod def empty(cls) -> "Domain": - return cls([], [], [], {}, [], {}) + """Returns empty Domain.""" + return Domain.from_dict({}) @classmethod def load(cls, paths: Union[List[Union[Path, Text]], Text, Path]) -> "Domain": + """Returns loaded Domain after merging all domain files.""" if not paths: raise InvalidDomain( "No domain file was specified. Please specify a path " @@ -153,6 +168,7 @@ def load(cls, paths: Union[List[Union[Path, Text]], Text, Path]) -> "Domain": @classmethod def from_path(cls, path: Union[Text, Path]) -> "Domain": + """Loads the `Domain` from a path.""" path = os.path.abspath(path) if os.path.isfile(path): @@ -194,17 +210,19 @@ def from_dict(cls, data: Dict) -> "Domain": Args: data: The serialized domain. - duplicates: A dictionary where keys are `intents`, `slots`, `forms` and - `responses` and values are lists of duplicated entries of a - corresponding type when the domain is built from multiple files. Returns: The instantiated `Domain` object. """ + duplicates = data.pop("duplicates", None) + if duplicates: + warn_about_duplicates_found_during_domain_merging(duplicates) + responses = data.get(KEY_RESPONSES, {}) domain_slots = data.get(KEY_SLOTS, {}) - rasa.shared.core.slot_mappings.validate_slot_mappings(domain_slots) + if domain_slots: + rasa.shared.core.slot_mappings.validate_slot_mappings(domain_slots) slots = cls.collect_slots(domain_slots) additional_arguments = data.get("config", {}) @@ -213,7 +231,6 @@ def from_dict(cls, data: Dict) -> "Domain": forms = data.get(KEY_FORMS, {}) _validate_forms(forms) - duplicates = data.get("duplicates", None) return cls( intents=intents, @@ -222,9 +239,9 @@ def from_dict(cls, data: Dict) -> "Domain": responses=responses, action_names=data.get(KEY_ACTIONS, []), forms=data.get(KEY_FORMS, {}), + data=Domain._cleaned_data(data), action_texts=data.get(KEY_E2E_ACTIONS, []), session_config=session_config, - duplicates=duplicates, **additional_arguments, ) @@ -253,18 +270,25 @@ def from_directory(cls, path: Text) -> "Domain": other_dict = rasa.shared.utils.io.read_yaml( rasa.shared.utils.io.read_file(full_path) ) - domain_dict = Domain.merge_domain_dicts( - cls, domain_dict, other_dict - ) + domain_dict = Domain.merge_domain_dicts(other_dict, domain_dict) + domain = Domain.from_dict(domain_dict) return domain - def merge(self, domain: Optional["Domain"], override: bool = False) -> "Domain": - """Merge this domain with another one, combining their attributes. + def merge( + self, + domain: Optional["Domain"], + override: bool = False, + ) -> "Domain": + """Merges this domain dict with another one, combining their attributes. + + This method merges domain dicts, and ensures all attributes (like ``intents``, + ``entities``, and ``actions``) are known to the Domain when the + object is created. - List attributes like ``intents`` and ``actions`` will be deduped - and merged. Single attributes will be taken from `self` unless - override is `True`, in which case they are taken from `domain`. + List attributes like ``intents`` and ``actions`` are deduped + and merged. Single attributes are taken from `domain1` unless + override is `True`, in which case they are taken from `domain2`. """ if not domain or domain.is_empty(): return self @@ -272,177 +296,153 @@ def merge(self, domain: Optional["Domain"], override: bool = False) -> "Domain": if self.is_empty(): return domain - domain_dict = domain.as_dict() - combined = self.as_dict() - - if override: - config = domain_dict["config"] - for key, val in config.items(): - combined["config"][key] = val - - if override or self.session_config == SessionConfig.default(): - combined[SESSION_CONFIG_KEY] = domain_dict[SESSION_CONFIG_KEY] - - for key in [KEY_INTENTS, KEY_ENTITIES]: - if combined[key] or domain_dict[key]: - combined[key] = self.merge_lists_of_dicts( - combined[key], domain_dict[key], override - ) - # remove existing forms from new actions - for form in combined[KEY_FORMS]: - if form in domain_dict[KEY_ACTIONS]: - domain_dict[KEY_ACTIONS].remove(form) - - for key in [KEY_ACTIONS, KEY_E2E_ACTIONS]: - combined[key] = self.merge_lists(combined[key], domain_dict[key]) - - for key in [KEY_FORMS, KEY_RESPONSES, KEY_SLOTS]: - combined[key] = self.merge_dicts(combined[key], domain_dict[key], override) + merged_dict = self.__class__.merge_domain_dicts( + domain.as_dict(), self.as_dict(), override + ) - return self.__class__.from_dict(combined) + return Domain.from_dict(merged_dict) + @staticmethod def merge_domain_dicts( - self, domain1: Dict, domain2: Dict, override: bool = False - ) -> Dict[Text, Any]: - """Merges this domain dict with another one, combining their attributes. + domain_dict: Dict, + combined: Dict, + override: bool = False, + ) -> Dict: + """Combines two domain dictionaries.""" + if not domain_dict: + return combined - This is used when multiple domain yml files are configured in a single - directory. Unlike the merge method above, which merges Domain objects by - creating each object then merging it with the previous, this method merges - domain dicts, and ensures all attributes (like ``intents``, ``entities``, and - ``actions``) are known to the Domain when the object is created. - - List attributes like ``intents`` and ``actions`` are deduped - and merged. Single attributes are taken from `domain1` unless - override is `True`, in which case they are taken from `domain2`. - """ - if not domain2: - return domain1 - - if not domain1: - return domain2 - - domain_dict = domain2 - combined = domain1 + if not combined: + return domain_dict if override: - config = domain_dict["config"] + config = domain_dict.get("config", {}) for key, val in config.items(): combined["config"][key] = val - if override or domain2.get("session_config"): + if ( + override + or combined.get(SESSION_CONFIG_KEY) == SessionConfig.default().as_dict() + or combined.get(SESSION_CONFIG_KEY) is None + ) and domain_dict.get(SESSION_CONFIG_KEY): combined[SESSION_CONFIG_KEY] = domain_dict[SESSION_CONFIG_KEY] - duplicates: Dict[Text, List[Text]] = {} - - for key in [KEY_INTENTS, KEY_ENTITIES]: - if combined.get(key) or domain_dict.get(key): - duplicates[key] = self.extract_duplicates( - combined.get(key, []), domain_dict.get(key, []) - ) - combined[key] = combined.get(key, []) - domain_dict[key] = domain_dict.get(key, []) - combined[key] = self.merge_lists_of_dicts( - combined[key], domain_dict[key], override - ) - # remove existing forms from new actions for form in combined.get(KEY_FORMS, []): if form in domain_dict.get(KEY_ACTIONS, []): domain_dict[KEY_ACTIONS].remove(form) - for key in [KEY_ACTIONS, KEY_E2E_ACTIONS]: - duplicates[key] = self.extract_duplicates( - combined.get(key, []), domain_dict.get(key, []) - ) - combined[key] = self.merge_lists( - combined.get(key, []), domain_dict.get(key, []) - ) + duplicates: Dict[Text, List[Text]] = {} + + merge_func_mappings: Dict[Text, Callable[..., Any]] = { + KEY_INTENTS: rasa.shared.utils.common.merge_lists_of_dicts, + KEY_ENTITIES: rasa.shared.utils.common.merge_lists_of_dicts, + KEY_ACTIONS: rasa.shared.utils.common.merge_lists, + KEY_E2E_ACTIONS: rasa.shared.utils.common.merge_lists, + KEY_FORMS: rasa.shared.utils.common.merge_dicts, + KEY_RESPONSES: rasa.shared.utils.common.merge_dicts, + KEY_SLOTS: rasa.shared.utils.common.merge_dicts, + } - for key in [KEY_FORMS, KEY_RESPONSES, KEY_SLOTS]: - duplicates[key] = self.extract_duplicates( + for key, merge_func in merge_func_mappings.items(): + duplicates[key] = rasa.shared.utils.common.extract_duplicates( combined.get(key, []), domain_dict.get(key, []) ) - combined[key] = self.merge_dicts( - combined.get(key, {}), domain_dict.get(key, {}), override + + if merge_func == rasa.shared.utils.common.merge_dicts: + default = {} + else: + default = [] + + combined[key] = merge_func( + combined.get(key, default), domain_dict.get(key, default), override ) if duplicates: - duplicates = self.clean_duplicates(duplicates) + duplicates = rasa.shared.utils.common.clean_duplicates(duplicates) combined.update({"duplicates": duplicates}) - return combined - @staticmethod - def extract_duplicates(list1: List[Any], list2: List[Any]) -> List[Any]: - """Extracts duplicates from two lists.""" - if list1: - dict1 = { - (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i - for i in list1 - } - else: - dict1 = {} + return combined - if list2: - dict2 = { - (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i - for i in list2 - } - else: - dict2 = {} + def _preprocess_domain_dict( + self, + data: Dict, + store_entities_as_slots: bool, + session_config: SessionConfig, + ) -> Dict: + data = self._add_default_keys_to_domain_dict( + data, + store_entities_as_slots, + session_config, + ) + data = self._sanitize_intents_in_domain_dict(data) - set1 = set(dict1.keys()) - set2 = set(dict2.keys()) - dupes = set1.intersection(set2) - return sorted(list(dupes)) + return data @staticmethod - def clean_duplicates(dupes: Dict[Text, Any]) -> Dict[Text, Any]: - """Removes keys for empty values.""" - duplicates = dupes.copy() - for k in dupes: - if not dupes[k]: - duplicates.pop(k) + def _add_default_keys_to_domain_dict( + data: Dict, + store_entities_as_slots: bool, + session_config: SessionConfig, + ) -> Dict: + # add the config, session_config and training data version defaults + # if not included in the original domain dict + if "config" not in data and not store_entities_as_slots: + data.update( + {"config": {"store_entities_as_slots": store_entities_as_slots}} + ) - return duplicates + if SESSION_CONFIG_KEY not in data: + data.update( + { + SESSION_CONFIG_KEY: { + SESSION_EXPIRATION_TIME_KEY: ( + session_config.session_expiration_time + ), + CARRY_OVER_SLOTS_KEY: session_config.carry_over_slots, + } + } + ) - @staticmethod - def merge_dicts( - tempDict1: Dict[Text, Any], - tempDict2: Dict[Text, Any], - override_existing_values: bool = False, - ) -> Dict[Text, Any]: - """Merges two dicts.""" - if override_existing_values: - merged_dicts, b = tempDict1.copy(), tempDict2.copy() + if KEY_TRAINING_DATA_FORMAT_VERSION not in data: + data.update( + { + KEY_TRAINING_DATA_FORMAT_VERSION: DoubleQuotedScalarString( + LATEST_TRAINING_DATA_FORMAT_VERSION + ) + } + ) - else: - merged_dicts, b = tempDict2.copy(), tempDict1.copy() - merged_dicts.update(b) - return merged_dicts + return data @staticmethod - def merge_lists(list1: List[Any], list2: List[Any]) -> List[Any]: - """Merges two lists.""" - return sorted(list(set(list1 + list2))) + def _reset_intent_flags(intent: Dict[Text, Any]) -> None: + for intent_property in intent.values(): + if ( + USE_ENTITIES_KEY in intent_property.keys() + and not intent_property[USE_ENTITIES_KEY] + ): + intent_property[USE_ENTITIES_KEY] = [] + if ( + IGNORE_ENTITIES_KEY in intent_property.keys() + and not intent_property[IGNORE_ENTITIES_KEY] + ): + intent_property[IGNORE_ENTITIES_KEY] = [] @staticmethod - def merge_lists_of_dicts( - dict_list1: List[Dict], - dict_list2: List[Dict], - override_existing_values: bool = False, - ) -> List[Dict]: - """Merges two dict lists.""" - dict1 = { - (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i - for i in dict_list1 - } - dict2 = { - (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i - for i in dict_list2 - } - merged_dicts = Domain.merge_dicts(dict1, dict2, override_existing_values) - return list(merged_dicts.values()) + def _sanitize_intents_in_domain_dict(data: Dict[Text, Any]) -> Dict[Text, Any]: + if not data.get(KEY_INTENTS): + return data + + for intent in data.get(KEY_INTENTS): + if isinstance(intent, dict): + Domain._reset_intent_flags(intent) + + data[KEY_INTENTS] = Domain._sort_intent_names_alphabetical_order( + intents=data.get(KEY_INTENTS) + ) + + return data @staticmethod def collect_slots(slot_dict: Dict[Text, Any]) -> List[Slot]: @@ -699,10 +699,10 @@ def __init__( responses: Dict[Text, List[Dict[Text, Any]]], action_names: List[Text], forms: Union[Dict[Text, Any], List[Text]], + data: Dict, action_texts: Optional[List[Text]] = None, store_entities_as_slots: bool = True, session_config: SessionConfig = SessionConfig.default(), - duplicates: Optional[Dict[Text, List[Text]]] = None, ) -> None: """Creates a `Domain`. @@ -714,14 +714,12 @@ def __init__( will send the matching response to the user. action_names: Names of custom actions. forms: Form names and their slot mappings. + data: original domain dict representation. action_texts: End-to-End bot utterances from end-to-end stories. store_entities_as_slots: If `True` Rasa will automatically create `SlotSet` events for entities if there are slots with the same name as the entity. session_config: Configuration for conversation sessions. Conversations are restarted at the end of a session. - duplicates: A dictionary where keys are `intents`, `slots`, `forms` and - `responses` and values are lists of duplicated entries of a - corresponding type when the domain is built from multiple files. """ self.entities, self.roles, self.groups = self.collect_entity_properties( entities @@ -740,9 +738,16 @@ def __init__( self.responses = responses - self.action_texts = action_texts or [] + self.action_texts = action_texts if action_texts is not None else [] + + data_copy = copy.deepcopy(data) + self._data = self._preprocess_domain_dict( + data_copy, + store_entities_as_slots, + session_config, + ) + self.session_config = session_config - self.duplicates = duplicates self._custom_actions = action_names @@ -846,14 +851,30 @@ def fingerprint(self) -> Text: fingerprint of the domain """ self_as_dict = self.as_dict() - self_as_dict[ - KEY_INTENTS - ] = rasa.shared.utils.common.sort_list_of_dicts_by_first_key( - self_as_dict[KEY_INTENTS] - ) + transformed_intents: List[Text] = [] + for intent in self_as_dict.get(KEY_INTENTS, []): + if isinstance(intent, dict): + transformed_intents.append(*intent.keys()) + elif isinstance(intent, str): + transformed_intents.append(intent) + + self_as_dict[KEY_INTENTS] = sorted(transformed_intents) self_as_dict[KEY_ACTIONS] = self.action_names_or_texts return rasa.shared.utils.io.get_dictionary_fingerprint(self_as_dict) + @staticmethod + def _sort_intent_names_alphabetical_order( + intents: List[Union[Text, Dict]] + ) -> List[Union[Text, Dict]]: + def sort(elem: Union[Text, Dict]) -> Union[Text, Dict]: + if isinstance(elem, dict): + return list(elem.keys())[0] + elif isinstance(elem, str): + return elem + + sorted_intents = sorted(intents, key=sort) + return sorted_intents + @rasa.shared.utils.common.lazy_property def user_actions_and_forms(self) -> List[Text]: """Returns combination of user actions and forms.""" @@ -1417,29 +1438,9 @@ def compare_with_specification(self, path: Text) -> bool: else: return True - def _slot_definitions(self) -> Dict[Any, Dict[str, Any]]: - # Only persist slots defined by the user. We add the default slots on the - # fly when loading the domain. - return {slot.name: slot.persistence_info() for slot in self._user_slots} - def as_dict(self) -> Dict[Text, Any]: """Return serialized `Domain`.""" - return { - "config": {"store_entities_as_slots": self.store_entities_as_slots}, - SESSION_CONFIG_KEY: { - SESSION_EXPIRATION_TIME_KEY: ( - self.session_config.session_expiration_time - ), - CARRY_OVER_SLOTS_KEY: self.session_config.carry_over_slots, - }, - KEY_INTENTS: self._transform_intents_for_file(), - KEY_ENTITIES: self._transform_entities_for_file(), - KEY_SLOTS: self._slot_definitions(), - KEY_RESPONSES: self.responses, - KEY_ACTIONS: self._custom_actions, - KEY_FORMS: self.forms, - KEY_E2E_ACTIONS: self.action_texts, - } + return self._data @staticmethod def get_responses_with_multilines( @@ -1468,141 +1469,29 @@ def get_responses_with_multilines( return final_responses - def _transform_intents_for_file( - self, - ) -> List[Dict[Text, Dict[Text, Union[bool, List[Text]]]]]: - """Transform intent properties for displaying or writing into a domain file. - - Internally, there is a property `used_entities` that lists all entities to be - used. In domain files, `use_entities` or `ignore_entities` is used instead to - list individual entities to ex- or include, because this is easier to read. - - Returns: - The intent properties as they are used in domain files. - """ - intent_properties = copy.deepcopy(self.intent_properties) - sorted_intent_properties = sorted(intent_properties.items()) - intents_for_file = [] - - for intent_name, intent_props in sorted_intent_properties: - if ( - intent_name in rasa.shared.core.constants.DEFAULT_INTENTS - and intent_name not in self.overridden_default_intents - ): - # Default intents should be not dumped with the domain - continue - # `use_entities` and `ignore_entities` in the domain file do not consider - # the role and group labels remove them from the list to make sure to not - # put them into the domain file - use_entities = set( - entity - for entity in intent_props[USED_ENTITIES_KEY] - if rasa.shared.core.constants.ENTITY_LABEL_SEPARATOR not in entity - ) - ignore_entities = set(self.entities) - use_entities - if len(use_entities) == len(self.entities): - intent_props[USE_ENTITIES_KEY] = True - elif len(use_entities) <= len(self.entities) / 2: - entities = list(use_entities) - entities.sort() - intent_props[USE_ENTITIES_KEY] = entities - else: - entities = list(ignore_entities) - entities.sort() - intent_props[IGNORE_ENTITIES_KEY] = entities - intent_props.pop(USED_ENTITIES_KEY) - intents_for_file.append({intent_name: intent_props}) - - return intents_for_file - - def _transform_entities_for_file(self) -> List[Union[Text, Dict[Text, Any]]]: - """Transform entity properties for displaying or writing to a domain file. - - Returns: - The entity properties as they are used in domain files. - """ - entities_for_file: List[Union[Text, Dict[Text, Any]]] = [] - - for entity in self.entities: - if entity in self.roles and entity in self.groups: - entities_for_file.append( - { - entity: { - ENTITY_GROUPS_KEY: self.groups[entity], - ENTITY_ROLES_KEY: self.roles[entity], - } - } - ) - elif entity in self.roles: - entities_for_file.append( - {entity: {ENTITY_ROLES_KEY: self.roles[entity]}} - ) - elif entity in self.groups: - entities_for_file.append( - {entity: {ENTITY_GROUPS_KEY: self.groups[entity]}} - ) - else: - entities_for_file.append(entity) - - return entities_for_file - - def cleaned_domain(self) -> Dict[Text, Any]: - """Fetch cleaned domain to display or write into a file. - - The internal `used_entities` property is replaced by `use_entities` or - `ignore_entities` and redundant keys are replaced with default values - to make the domain easier readable. + @staticmethod + def _cleaned_data(data: Dict[Text, Any]) -> Dict[Text, Any]: + """Remove empty and redundant keys from merged domain dict. Returns: A cleaned dictionary version of the domain. """ - domain_data = self.as_dict() - # remove e2e actions from domain before we display it - domain_data.pop(KEY_E2E_ACTIONS, None) - - for idx, intent_info in enumerate(domain_data[KEY_INTENTS]): - for name, intent in intent_info.items(): - if intent.get(USE_ENTITIES_KEY) is True: - del intent[USE_ENTITIES_KEY] - if not intent.get(IGNORE_ENTITIES_KEY): - intent.pop(IGNORE_ENTITIES_KEY, None) - if len(intent) == 0: - domain_data[KEY_INTENTS][idx] = name - - for slot in domain_data[KEY_SLOTS].values(): - if slot["initial_value"] is None: - del slot["initial_value"] - if slot["type"].startswith("rasa.shared.core.slots"): - slot["type"] = Slot.resolve_by_type(slot["type"]).type_name - - if domain_data["config"]["store_entities_as_slots"]: - del domain_data["config"]["store_entities_as_slots"] - - # clean empty keys return { key: val - for key, val in domain_data.items() + for key, val in data.items() if val != {} and val != [] and val is not None } def persist(self, filename: Union[Text, Path]) -> None: """Write domain to a file.""" - as_yaml = self.as_yaml(clean_before_dump=False) - rasa.shared.utils.io.write_text_file(as_yaml, filename) - - def persist_clean(self, filename: Union[Text, Path]) -> None: - """Write cleaned domain to a file.""" - as_yaml = self.as_yaml(clean_before_dump=True) + as_yaml = self.as_yaml() rasa.shared.utils.io.write_text_file(as_yaml, filename) - def as_yaml(self, clean_before_dump: bool = False) -> Text: + def as_yaml(self) -> Text: """Dump the `Domain` object as a YAML string. + This function preserves the orders of the keys in the domain. - Args: - clean_before_dump: When set to `True`, this method returns - a version of the domain without internal - information. Defaults to `False`. Returns: A string in YAML format representing the domain. """ @@ -1614,10 +1503,9 @@ def as_yaml(self, clean_before_dump: bool = False) -> Text: LATEST_TRAINING_DATA_FORMAT_VERSION ) } - if clean_before_dump: - domain_data.update(self.cleaned_domain()) - else: - domain_data.update(self.as_dict()) + + domain_data.update(self.as_dict()) + if domain_data.get(KEY_RESPONSES, {}): domain_data[KEY_RESPONSES] = self.get_responses_with_multilines( domain_data[KEY_RESPONSES] @@ -1642,7 +1530,6 @@ def _slots_for_domain_warnings(self) -> List[Text]: Excludes slots which aren't featurized. """ - return [slot.name for slot in self._user_slots if slot.influence_conversation] @property @@ -1651,7 +1538,6 @@ def _actions_for_domain_warnings(self) -> List[Text]: Includes user and form actions, but excludes those that are default actions. """ - return [ action for action in self.user_actions_and_forms @@ -1663,15 +1549,16 @@ def _get_symmetric_difference( domain_elements: Union[List[Text], Set[Text]], training_data_elements: Optional[Union[List[Text], Set[Text]]], ) -> Dict[Text, Set[Text]]: - """Get symmetric difference between a set of domain elements and a set of - training data elements. + """Gets the symmetric difference between two sets. + + One set represents domain elements and the other one is a set of training + data elements. Returns a dictionary containing a list of items found in the `domain_elements` but not in `training_data_elements` at key `in_domain`, and a list of items found in `training_data_elements` but not in `domain_elements` at key `in_training_data_set`. """ - if training_data_elements is None: training_data_elements = set() @@ -1948,6 +1835,35 @@ def __repr__(self) -> Text: ) +def warn_about_duplicates_found_during_domain_merging( + duplicates: Dict[Text, List[Text]] +) -> None: + """Emits warning about found duplicates while loading multiple domain paths.""" + message = "" + for key in [ + KEY_INTENTS, + KEY_FORMS, + KEY_ACTIONS, + KEY_E2E_ACTIONS, + KEY_RESPONSES, + KEY_SLOTS, + KEY_ENTITIES, + ]: + duplicates_per_key = duplicates.get(key) + if duplicates_per_key: + if message: + message += " \n" + + duplicates_per_key_str = ", ".join(duplicates_per_key) + message += ( + f"The following duplicated {key} have been found " + f"across multiple domain files: {duplicates_per_key_str}" + ) + + rasa.shared.utils.io.raise_warning(message, docs=DOCS_URL_DOMAINS) + return None + + def _validate_forms(forms: Union[Dict, List]) -> None: if not isinstance(forms, dict): raise InvalidDomain("Forms have to be specified as dictionary.") diff --git a/rasa/shared/importers/importer.py b/rasa/shared/importers/importer.py index 7e4e4a3a0ef7..247dd89de2ec 100644 --- a/rasa/shared/importers/importer.py +++ b/rasa/shared/importers/importer.py @@ -6,7 +6,13 @@ import rasa.shared.utils.common import rasa.shared.core.constants import rasa.shared.utils.io -from rasa.shared.core.domain import Domain +from rasa.shared.core.domain import ( + Domain, + KEY_E2E_ACTIONS, + KEY_INTENTS, + KEY_RESPONSES, + KEY_ACTIONS, +) from rasa.shared.core.events import ActionExecuted, UserUttered from rasa.shared.core.training_data.structures import StoryGraph from rasa.shared.nlu.training_data.message import Message @@ -241,7 +247,9 @@ def get_domain(self) -> Domain: domains = [importer.get_domain() for importer in self._importers] return reduce( - lambda merged, other: merged.merge(other), domains, Domain.empty() + lambda merged, other: merged.merge(other), + domains, + Domain.empty(), ) @rasa.shared.utils.common.cached_method @@ -322,7 +330,9 @@ def get_domain(self) -> Domain: existing_domain, ) - existing_domain = existing_domain.merge(domain_with_retrieval_intents) + existing_domain = existing_domain.merge( + domain_with_retrieval_intents, override=True + ) existing_domain.check_missing_responses() return existing_domain @@ -374,13 +384,16 @@ def _get_domain_with_retrieval_intents( intent_properties[IS_RETRIEVAL_INTENT_KEY] = True retrieval_intent_properties.append({intent: intent_properties}) - return Domain( - retrieval_intent_properties, - [], - [], - responses, - ResponsesSyncImporter._construct_retrieval_action_names(retrieval_intents), - {}, + action_names = ResponsesSyncImporter._construct_retrieval_action_names( + retrieval_intents + ) + + return Domain.from_dict( + { + KEY_INTENTS: retrieval_intent_properties, + KEY_RESPONSES: responses, + KEY_ACTIONS: action_names, + } ) def get_stories(self, exclusion_percentage: Optional[int] = None) -> StoryGraph: @@ -435,6 +448,7 @@ def get_domain(self) -> Domain: """Retrieves model domain (see parent class for full docstring).""" original = self.importer.get_domain() e2e_domain = self._get_domain_with_e2e_actions() + return original.merge(e2e_domain) def _get_domain_with_e2e_actions(self) -> Domain: @@ -453,15 +467,7 @@ def _get_domain_with_e2e_actions(self) -> Domain: additional_e2e_action_names = list(additional_e2e_action_names) - return Domain( - [], - [], - [], - {}, - action_names=[], - forms={}, - action_texts=additional_e2e_action_names, - ) + return Domain.from_dict({KEY_E2E_ACTIONS: additional_e2e_action_names}) def get_stories(self, exclusion_percentage: Optional[int] = None) -> StoryGraph: """Retrieves the stories that should be used for training. diff --git a/rasa/shared/importers/multi_project.py b/rasa/shared/importers/multi_project.py index 79078066b6af..18ccdaa948ee 100644 --- a/rasa/shared/importers/multi_project.py +++ b/rasa/shared/importers/multi_project.py @@ -179,7 +179,9 @@ def get_domain(self) -> Domain: """Retrieves model domain (see parent class for full docstring).""" domains = [Domain.load(path) for path in self._domain_paths] return reduce( - lambda merged, other: merged.merge(other), domains, Domain.empty() + lambda merged, other: merged.merge(other), + domains, + Domain.empty(), ) def get_stories(self, exclusion_percentage: Optional[int] = None) -> StoryGraph: diff --git a/rasa/shared/utils/common.py b/rasa/shared/utils/common.py index aafda03e92eb..ed690b442362 100644 --- a/rasa/shared/utils/common.py +++ b/rasa/shared/utils/common.py @@ -201,3 +201,73 @@ def arguments_of(func: Callable) -> List[Text]: import inspect return list(inspect.signature(func).parameters.keys()) + + +def extract_duplicates(list1: List[Any], list2: List[Any]) -> List[Any]: + """Extracts duplicates from two lists.""" + if list1: + dict1 = { + (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i for i in list1 + } + else: + dict1 = {} + + if list2: + dict2 = { + (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i for i in list2 + } + else: + dict2 = {} + + set1 = set(dict1.keys()) + set2 = set(dict2.keys()) + dupes = set1.intersection(set2) + return sorted(list(dupes)) + + +def clean_duplicates(dupes: Dict[Text, Any]) -> Dict[Text, Any]: + """Removes keys for empty values.""" + duplicates = dupes.copy() + for k in dupes: + if not dupes[k]: + duplicates.pop(k) + + return duplicates + + +def merge_dicts( + tempDict1: Dict[Text, Any], + tempDict2: Dict[Text, Any], + override_existing_values: bool = False, +) -> Dict[Text, Any]: + """Merges two dicts.""" + if override_existing_values: + merged_dicts, b = tempDict1.copy(), tempDict2.copy() + + else: + merged_dicts, b = tempDict2.copy(), tempDict1.copy() + merged_dicts.update(b) + return merged_dicts + + +def merge_lists( + list1: List[Any], list2: List[Any], override: bool = False +) -> List[Any]: + """Merges two lists.""" + return sorted(list(set(list1 + list2))) + + +def merge_lists_of_dicts( + dict_list1: List[Dict], + dict_list2: List[Dict], + override_existing_values: bool = False, +) -> List[Dict]: + """Merges two dict lists.""" + dict1 = { + (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i for i in dict_list1 + } + dict2 = { + (sorted(list(i.keys()))[0] if isinstance(i, dict) else i): i for i in dict_list2 + } + merged_dicts = merge_dicts(dict1, dict2, override_existing_values) + return list(merged_dicts.values()) diff --git a/rasa/validator.py b/rasa/validator.py index f91087459994..30b67ab66a54 100644 --- a/rasa/validator.py +++ b/rasa/validator.py @@ -16,13 +16,7 @@ from rasa.shared.core.constants import MAPPING_CONDITIONS, ACTIVE_LOOP from rasa.shared.core.events import ActionExecuted, ActiveLoop from rasa.shared.core.events import UserUttered -from rasa.shared.core.domain import ( - KEY_INTENTS, - KEY_RESPONSES, - KEY_SLOTS, - KEY_FORMS, - Domain, -) +from rasa.shared.core.domain import Domain from rasa.shared.core.generator import TrainingDataGenerator from rasa.shared.core.constants import SlotMappingType, MAPPING_TYPE from rasa.shared.core.training_data.structures import StoryGraph @@ -331,32 +325,6 @@ def verify_nlu(self, ignore_warnings: bool = True) -> bool: stories_are_valid = self.verify_utterances_in_stories(ignore_warnings) return intents_are_valid and stories_are_valid and there_is_no_duplication - def verify_domain_duplicates(self) -> bool: - """Verifies that there are no duplicated dictionaries in multiple domain files. - - Returns: - `True` if duplicates exist. - """ - logger.info("Checking duplicates across domain files...") - - all_valid = True - - if not self.domain.duplicates: - return True - - for key in [KEY_INTENTS, KEY_FORMS, KEY_RESPONSES, KEY_SLOTS]: - duplicates = self.domain.duplicates.get(key) - if duplicates: - duplicates_str = ", ".join(duplicates) - rasa.shared.utils.io.raise_warning( - f"The following duplicated {key} has been found " - + f"across multiple domain files: {duplicates_str}", - docs=DOCS_URL_DOMAINS, - ) - all_valid = False - - return all_valid - def verify_form_slots(self) -> bool: """Verifies that form slots match the slot mappings in domain.""" domain_slot_names = [slot.name for slot in self.domain.slots] diff --git a/tests/core/evaluation/test_marker.py b/tests/core/evaluation/test_marker.py index ea4536fcb2ff..1dc88c095ba2 100644 --- a/tests/core/evaluation/test_marker.py +++ b/tests/core/evaluation/test_marker.py @@ -568,7 +568,7 @@ def test_domain_validation_with_valid_marker(depth: int, max_branches: int, seed slots = [Slot(name, []) for name in _collect_parameters(marker, SlotSetMarker)] actions = list(_collect_parameters(marker, ActionExecutedMarker)) intents = _collect_parameters(marker, IntentDetectedMarker) - domain = Domain(intents, [], slots, {}, actions, {}) + domain = Domain(intents, [], slots, {}, actions, {}, {}) assert marker.validate_against_domain(domain) diff --git a/tests/core/featurizers/test_precomputation.py b/tests/core/featurizers/test_precomputation.py index 4fef7d71a515..2ab4880d851c 100644 --- a/tests/core/featurizers/test_precomputation.py +++ b/tests/core/featurizers/test_precomputation.py @@ -353,6 +353,7 @@ def test_container_derive_messages_from_domain_and_add(): entities=["e_a", "e_b", "e_c"], slots=[Slot(name="s", mappings=[{}])], forms=forms, + data={}, ) lookup_table = MessageContainerForCoreFeaturization() lookup_table.derive_messages_from_domain_and_add(domain) @@ -382,6 +383,7 @@ def test_converter_for_training(input_converter: CoreFeaturizationInputConverter responses=dict(), action_names=["action_listen", "utter_greet"], forms=dict(), + data={}, action_texts=["Hi how are you?"], ) events = [ diff --git a/tests/core/featurizers/test_single_state_featurizers.py b/tests/core/featurizers/test_single_state_featurizers.py index cf84cf55e194..8293f79216d3 100644 --- a/tests/core/featurizers/test_single_state_featurizers.py +++ b/tests/core/featurizers/test_single_state_featurizers.py @@ -93,6 +93,7 @@ def test_prepare_for_training(): responses={}, forms={}, action_names=["utter_greet", "action_check_weather"], + data={}, ) f = SingleStateFeaturizer() @@ -125,6 +126,7 @@ def test_encode_all_labels__encoded_all_action_names_and_texts(): responses={}, forms={}, action_names=["a", "b", "c", "d"], + data={}, ) f = SingleStateFeaturizer() @@ -452,6 +454,7 @@ def test_encode_entities__with_entity_roles_and_groups(): responses={}, forms={}, action_names=[], + data={}, ) f = SingleStateFeaturizer() f.prepare_for_training(domain) @@ -484,6 +487,7 @@ def test_encode_entities__with_bilou_entity_roles_and_groups(): responses={}, forms={}, action_names=[], + data={}, ) f = SingleStateFeaturizer() f.prepare_for_training(domain, bilou_tagging=True) diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index 29f634fe08d1..6089f09f0c51 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -126,6 +126,7 @@ def test_domain_action_instantiation(): responses={}, action_names=["my_module.ActionTest", "utter_test", "utter_chitchat"], forms={}, + data={}, ) instantiated_actions = [ diff --git a/tests/core/test_processor.py b/tests/core/test_processor.py index 008527c31800..2e138733c3c1 100644 --- a/tests/core/test_processor.py +++ b/tests/core/test_processor.py @@ -1184,6 +1184,7 @@ async def test_logging_of_end_to_end_action( action_names=[], forms={}, action_texts=[end_to_end_action], + data={}, ) default_processor.domain = new_domain diff --git a/tests/examples/test_example_bots_training_data.py b/tests/examples/test_example_bots_training_data.py index eeb34b465598..e6d0b9ca1930 100644 --- a/tests/examples/test_example_bots_training_data.py +++ b/tests/examples/test_example_bots_training_data.py @@ -8,59 +8,76 @@ @pytest.mark.parametrize( - "config_file, domain_file, data_folder", + "config_file, domain_file, data_folder, raise_slot_warning", [ ( "examples/concertbot/config.yml", "examples/concertbot/domain.yml", "examples/concertbot/data", + True, ), ( "examples/formbot/config.yml", "examples/formbot/domain.yml", "examples/formbot/data", + True, ), ( "examples/knowledgebasebot/config.yml", "examples/knowledgebasebot/domain.yml", "examples/knowledgebasebot/data", + True, ), ( "data/test_moodbot/config.yml", "data/test_moodbot/domain.yml", "data/test_moodbot/data", + False, ), ( "examples/reminderbot/config.yml", "examples/reminderbot/domain.yml", "examples/reminderbot/data", + True, ), ( "examples/rules/config.yml", "examples/rules/domain.yml", "examples/rules/data", + True, ), ], ) def test_example_bot_training_data_raises_only_auto_fill_warning( - config_file: Text, domain_file: Text, data_folder: Text + config_file: Text, + domain_file: Text, + data_folder: Text, + raise_slot_warning: bool, ): importer = TrainingDataImporter.load_from_config( config_file, domain_file, [data_folder] ) - with pytest.warns(UserWarning) as record: - importer.get_nlu_data() - importer.get_stories() + if raise_slot_warning: + with pytest.warns(UserWarning) as record: + importer.get_nlu_data() + importer.get_stories() - # two for slot auto-fill removal - assert len(record) == 2 - assert ( - "Slot auto-fill has been removed in 3.0 and replaced with " - "a new explicit mechanism to set slots." in record[0].message.args[0] - ) - assert record[0].message.args[0] == record[1].message.args[0] + assert len(record) == 2 + assert all( + [ + "Slot auto-fill has been removed in 3.0 and replaced with " + "a new explicit mechanism to set slots." in r.message.args[0] + for r in record + ] + ) + else: + with pytest.warns(None) as record: + importer.get_nlu_data() + importer.get_stories() + + assert len(record) == 0 def test_example_bot_training_on_initial_project(tmp_path: Path): @@ -74,14 +91,8 @@ def test_example_bot_training_on_initial_project(tmp_path: Path): str(tmp_path / "data"), ) - with pytest.warns(UserWarning) as record: + with pytest.warns(None) as record: importer.get_nlu_data() importer.get_stories() - # two for slot auto-fill removal - assert len(record) == 2 - assert ( - "Slot auto-fill has been removed in 3.0 and replaced with " - "a new explicit mechanism to set slots." in record[0].message.args[0] - ) - assert record[0].message.args[0] == record[1].message.args[0] + assert len(record) == 0 diff --git a/tests/graph_components/validators/test_default_recipe_validator.py b/tests/graph_components/validators/test_default_recipe_validator.py index 669319c19d2a..c4d36469f44e 100644 --- a/tests/graph_components/validators/test_default_recipe_validator.py +++ b/tests/graph_components/validators/test_default_recipe_validator.py @@ -1026,16 +1026,9 @@ def test_no_warnings_with_default_project(tmp_path: Path): ) validator = DefaultV1RecipeValidator(graph_config.train_schema) - with pytest.warns( - UserWarning, match="Slot auto-fill has been removed in 3.0" - ) as records: + with pytest.warns(None) as records: validator.validate(importer) - assert all( - [ - warn.message.args[0].startswith("Slot auto-fill has been removed") - for warn in records.list - ] - ) + assert len(records) == 0 def test_importer_with_invalid_model_config(tmp_path: Path): diff --git a/tests/nlu/classifiers/test_regex_message_handler.py b/tests/nlu/classifiers/test_regex_message_handler.py index fd07337f30a8..ce685b429eb7 100644 --- a/tests/nlu/classifiers/test_regex_message_handler.py +++ b/tests/nlu/classifiers/test_regex_message_handler.py @@ -59,6 +59,7 @@ def test_process_does_not_do_anything( responses={}, action_names=[], forms={}, + data={}, ) parsed_messages = regex_message_handler.process([message], domain) diff --git a/tests/shared/core/test_domain.py b/tests/shared/core/test_domain.py index 8bc6deeb8dd2..c62a974c80da 100644 --- a/tests/shared/core/test_domain.py +++ b/tests/shared/core/test_domain.py @@ -323,12 +323,11 @@ def test_domain_to_dict(): domain_as_dict = Domain.from_yaml(test_yaml).as_dict() assert domain_as_dict == { + "version": LATEST_TRAINING_DATA_FORMAT_VERSION, "actions": ["action_save_world"], "config": {"store_entities_as_slots": True}, KEY_E2E_ACTIONS: ["Hello, dear user", "what's up"], - "entities": [], "forms": {"some_form": {"required_slots": []}}, - "intents": [], "responses": {"utter_greet": [{"text": "hey there!"}]}, "session_config": { "carry_over_slots_to_new_session": True, @@ -337,10 +336,8 @@ def test_domain_to_dict(): "slots": { "some_slot": { "values": ["high", "low"], - "influence_conversation": True, - "initial_value": None, "mappings": [{"type": "from_text"}], - "type": "rasa.shared.core.slots.CategoricalSlot", + "type": "categorical", } }, } @@ -348,7 +345,7 @@ def test_domain_to_dict(): def test_domain_to_yaml(): test_yaml = f""" -version: '3.0' +version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' actions: - action_save_world config: @@ -366,18 +363,25 @@ def test_domain_to_yaml(): slots: {{}} """ - with pytest.warns(UserWarning) as record: - domain = Domain.from_yaml(test_yaml) - actual_yaml = domain.as_yaml() + domain = Domain.from_yaml(test_yaml) + actual_yaml = domain.as_yaml() - assert ( - "Slot auto-fill has been removed in 3.0" - " and replaced with a new explicit mechanism to set slots. " - in record[0].message.args[0] - ) + expected_yaml = f""" +version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' +actions: +- action_save_world +config: + store_entities_as_slots: true +responses: + utter_greet: + - text: hey there! +session_config: + carry_over_slots_to_new_session: true + session_expiration_time: {DEFAULT_SESSION_EXPIRATION_TIME_IN_MINUTES} +""" - expected = rasa.shared.utils.io.read_yaml(test_yaml) actual = rasa.shared.utils.io.read_yaml(actual_yaml) + expected = rasa.shared.utils.io.read_yaml(expected_yaml) assert actual == expected @@ -417,7 +421,9 @@ def test_merge_yaml_domains(): domain_1 = Domain.from_yaml(test_yaml_1) domain_2 = Domain.from_yaml(test_yaml_2) + domain = domain_1.merge(domain_2) + # single attribute should be taken from domain_1 assert domain.store_entities_as_slots # conflicts should be taken from domain_1 @@ -456,6 +462,7 @@ def test_merge_yaml_domains_with_default_intents(default_intent: Text): domain_1 = Domain.from_yaml(test_yaml_1) domain_2 = Domain.from_yaml(test_yaml_2) + domain = domain_1.merge(domain_2) # check that the default intents were merged correctly @@ -463,11 +470,7 @@ def test_merge_yaml_domains_with_default_intents(default_intent: Text): assert domain.intents == sorted(["greet", *DEFAULT_INTENTS]) # ensure that the default intent is contain the domain's dictionary dump - domain_intents = [] - for intent in domain.as_dict()["intents"]: - domain_intents.append(list(intent)[0]) - - assert default_intent in domain_intents + assert default_intent in domain.as_dict()[KEY_INTENTS] def test_merge_session_config_if_first_is_not_default(): @@ -517,9 +520,8 @@ def test_merge_with_empty_domain(): - text: hey you! """ ) - - merged = Domain.empty().merge(domain) - + empty_domain = Domain.empty() + merged = empty_domain.merge(domain, override=True) assert merged.as_dict() == domain.as_dict() @@ -585,6 +587,7 @@ def test_merge_domain_with_forms(): domain_1 = Domain.from_yaml(test_yaml_1) domain_2 = Domain.from_yaml(test_yaml_2) + domain = domain_1.merge(domain_2) expected_number_of_forms = 3 @@ -968,6 +971,7 @@ def test_check_domain_sanity_on_invalid_domain(): responses={}, action_names=["random_name", "random_name"], forms={}, + data={}, ) with pytest.raises(InvalidDomain): @@ -981,6 +985,7 @@ def test_check_domain_sanity_on_invalid_domain(): responses={}, action_names=[], forms={}, + data={}, ) with pytest.raises(InvalidDomain): @@ -991,6 +996,7 @@ def test_check_domain_sanity_on_invalid_domain(): responses={}, action_names=[], forms={}, + data={}, ) @@ -1053,17 +1059,17 @@ def test_is_empty(): assert Domain.empty().is_empty() -def test_transform_intents_for_file_default(): +def test_load_intents_from_as_dict_representation(): domain_path = "data/test_domains/default_unfeaturized_entities.yml" domain = Domain.load(domain_path) - transformed = domain._transform_intents_for_file() + transformed = domain.as_dict().get(KEY_INTENTS) expected = [ {"ask": {USE_ENTITIES_KEY: True}}, {"default": {IGNORE_ENTITIES_KEY: ["unrelated_recognized_entity"]}}, {"goodbye": {USE_ENTITIES_KEY: []}}, {"greet": {USE_ENTITIES_KEY: ["name"]}}, - {"pure_intent": {USE_ENTITIES_KEY: True}}, + "pure_intent", {"thank": {USE_ENTITIES_KEY: []}}, {"why": {USE_ENTITIES_KEY: []}}, ] @@ -1071,19 +1077,19 @@ def test_transform_intents_for_file_default(): assert transformed == expected -def test_transform_intents_for_files_with_entities(): +def test_load_intents_with_entities_from_as_dict(): domain_path = "data/test_domains/test_domain_from_directory_for_entities" domain = Domain.load(domain_path) - transformed = domain._transform_intents_for_file() + transformed = domain.as_dict().get(KEY_INTENTS) expected = [ {"certify": {USE_ENTITIES_KEY: True}}, {"play": {USE_ENTITIES_KEY: ["ball", "chess"]}}, - {"question": {USE_ENTITIES_KEY: True}}, + "question", {"stow_away": {USE_ENTITIES_KEY: True}}, { "support_encouraging": { - USE_ENTITIES_KEY: ["anti_freeze_blankets", "automatic_cupcakes"] + USE_ENTITIES_KEY: ["automatic_cupcakes", "anti_freeze_blankets"] } }, {"vacationing": {"ignore_entities": ["tornadoes"]}}, @@ -1092,72 +1098,43 @@ def test_transform_intents_for_files_with_entities(): assert transformed == expected -def test_transform_intents_for_file_with_mapping(): +def test_load_intents_for_file_from_as_dict(): domain_path = "data/test_domains/default_with_mapping.yml" domain = Domain.load(domain_path) - transformed = domain._transform_intents_for_file() + transformed = domain.as_dict().get(KEY_INTENTS) expected = [ - {"default": {"triggers": "utter_default", USE_ENTITIES_KEY: True}}, - {"goodbye": {USE_ENTITIES_KEY: True}}, - {"greet": {"triggers": "utter_greet", USE_ENTITIES_KEY: True}}, + {"default": {"triggers": "utter_default"}}, + "goodbye", + {"greet": {"triggers": "utter_greet"}}, ] assert transformed == expected -def test_transform_intents_for_file_with_entity_roles_groups(): +def test_load_intents_with_entity_roles_groups_from_as_dict(): domain_path = "data/test_domains/travel_form.yml" domain = Domain.load(domain_path) - transformed = domain._transform_intents_for_file() + transformed = domain.as_dict().get(KEY_INTENTS) expected = [ - {"greet": {USE_ENTITIES_KEY: ["name"]}}, + {"greet": {IGNORE_ENTITIES_KEY: ["GPE"]}}, {"inform": {USE_ENTITIES_KEY: ["GPE"]}}, ] assert transformed == expected -def test_transform_entities_for_file_default(): +def test_load_entities_from_as_dict(): domain_path = "data/test_domains/travel_form.yml" domain = Domain.load(domain_path) - transformed = domain._transform_entities_for_file() + transformed = domain.as_dict().get(KEY_ENTITIES) expected = [{"GPE": {ENTITY_ROLES_KEY: ["destination", "origin"]}}, "name"] assert transformed == expected -def test_clean_domain_for_file(): - domain_path = "data/test_domains/default_unfeaturized_entities.yml" - cleaned = Domain.load(domain_path).cleaned_domain() - - expected = { - "entities": ["name", "unrelated_recognized_entity", "other"], - "intents": [ - "ask", - {"default": {IGNORE_ENTITIES_KEY: ["unrelated_recognized_entity"]}}, - {"goodbye": {USE_ENTITIES_KEY: []}}, - {"greet": {USE_ENTITIES_KEY: ["name"]}}, - "pure_intent", - {"thank": {USE_ENTITIES_KEY: []}}, - {"why": {USE_ENTITIES_KEY: []}}, - ], - "responses": { - "utter_default": [{"text": "default message"}], - "utter_goodbye": [{"text": "goodbye :("}], - "utter_greet": [{"text": "hey there!"}], - }, - "session_config": { - "carry_over_slots_to_new_session": True, - "session_expiration_time": DEFAULT_SESSION_EXPIRATION_TIME_IN_MINUTES, - }, - } - - assert cleaned == expected - - def test_not_add_knowledge_base_slots(): test_domain = Domain.empty() @@ -1226,8 +1203,7 @@ def test_session_config( def test_domain_as_dict_with_session_config(): session_config = SessionConfig(123, False) - domain = Domain.empty() - domain.session_config = session_config + domain = Domain([], [], [], {}, [], {}, {}, None, True, session_config) serialized = domain.as_dict() deserialized = Domain.from_dict(serialized) @@ -1586,7 +1562,7 @@ def test_invalid_slots_raises_yaml_exception(domain_yaml: Text): def test_slot_order_is_preserved(): - test_yaml = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" + test_yaml = f"""version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' session_config: session_expiration_time: 60 carry_over_slots_to_new_session: true @@ -1639,7 +1615,7 @@ def test_slot_order_is_preserved(): """ domain = Domain.from_yaml(test_yaml) - assert domain.as_yaml(clean_before_dump=True) == test_yaml + assert domain.as_yaml() == test_yaml def test_slot_order_is_preserved_when_merging(): @@ -1676,21 +1652,21 @@ def test_slot_order_is_preserved_when_merging(): """ test_yaml_merged = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" +slots:{slot_2}{slot_1} session_config: session_expiration_time: 60 carry_over_slots_to_new_session: true -slots:{slot_2}{slot_1} """ domain_1 = Domain.from_yaml(test_yaml_1) domain_2 = Domain.from_yaml(test_yaml_2) domain_merged = domain_1.merge(domain_2) - assert domain_merged.as_yaml(clean_before_dump=True) == test_yaml_merged + assert domain_merged.as_yaml() == test_yaml_merged def test_responses_text_multiline_is_preserved(): - test_yaml = f"""version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" + test_yaml = f"""version: '{LATEST_TRAINING_DATA_FORMAT_VERSION}' session_config: session_expiration_time: 60 carry_over_slots_to_new_session: true @@ -1707,7 +1683,7 @@ def test_responses_text_multiline_is_preserved(): """ domain = Domain.from_yaml(test_yaml) - assert domain.as_yaml(clean_before_dump=True) == test_yaml + assert domain.as_yaml() == test_yaml def test_is_valid_domain_doesnt_raise_with_valid_domain(tmpdir: Path): @@ -1821,63 +1797,21 @@ def test_domain_invalid_yml_in_folder(): Domain.from_directory("data/test_domains/test_domain_from_directory/") -def test_domain_with_duplicates(): - """ - Check if a domain with duplicated slots, responses and intents in domain files - removes the duplications in the domain. - """ - domain = Domain.from_directory("data/test_domains/test_domain_with_duplicates/") - expected_intents = [ - "affirm", - "back", - "bot_challenge", - "deny", - "goodbye", - "greet", - "mood_great", - "mood_unhappy", - "nlu_fallback", - "out_of_scope", - "restart", - "session_start", - "test", - ] - expected_responses = { - "utter_greet": [{"text": "Hey! How are you?"}], - "utter_did_that_help": [{"text": "Did that help you?"}], - "utter_happy": [{"text": "Great, carry on!"}], - "utter_cheer_up": [ - { - "text": "Here is something to cheer you up:", - "image": "https://i.imgur.com/nGF1K8f.jpg", - } - ], - "utter_goodbye": [{"text": "Bye"}], - "utter_iamabot": [{"text": "I am a bot, powered by Rasa."}], - } - assert domain.intents == expected_intents - assert domain.responses == expected_responses - assert domain.duplicates["slots"] == ["mood"] - assert domain.duplicates["responses"] == ["utter_did_that_help", "utter_greet"] - assert domain.duplicates["intents"] == ["greet"] - - -def test_domain_without_duplicates(): +def test_invalid_domain_dir_with_duplicates(): """ - Check if a domain without duplicated slots, responses and intents contains - nothing in `duplicates` field. + Raises InvalidDomain if a domain is loaded from a directory with duplicated slots, + responses and intents in domain files. """ - domain = Domain.from_directory("data/test_domains/test_domain_without_duplicates/") - assert domain.duplicates == {} - - -def test_domain_duplicates_when_one_domain_file(): - """ - Check if a domain with duplicated slots, responses and intents contains - a correct information in `duplicates` field. - """ - domain = Domain.from_file(path="data/test_domains/default.yml") - assert domain.duplicates is None + with pytest.warns(UserWarning) as warning: + Domain.from_directory("data/test_domains/test_domain_with_duplicates/") + + error_message = ( + "The following duplicated intents have been found across multiple domain files: greet \n" + "The following duplicated responses have been found across multiple domain files: " + "utter_did_that_help, utter_greet \n" + "The following duplicated slots have been found across multiple domain files: mood" + ) + assert error_message == warning[2].message.args[0] def test_domain_fingerprint_consistency_across_runs(): diff --git a/tests/shared/core/training_data/story_reader/test_yaml_story_reader.py b/tests/shared/core/training_data/story_reader/test_yaml_story_reader.py index 44a173cb31cf..51521fc4a263 100644 --- a/tests/shared/core/training_data/story_reader/test_yaml_story_reader.py +++ b/tests/shared/core/training_data/story_reader/test_yaml_story_reader.py @@ -977,6 +977,7 @@ def test_process_unpacks_attributes_from_single_message_and_fallsback_if_needed( responses={}, action_names=[], forms={}, + data={}, ) # extract information @@ -1068,6 +1069,7 @@ def test_process_warns_if_intent_or_entities_not_in_domain( responses={}, action_names=[], forms={}, + data={}, ) # expect a warning @@ -1097,6 +1099,7 @@ async def test_unpack_regex_message_has_correct_entity_start_and_end(): responses={}, action_names=[], forms={}, + data={}, ) message = YAMLStoryReader.unpack_regex_message( diff --git a/tests/shared/utils/test_common.py b/tests/shared/utils/test_common.py index 323a431bf17a..4753300677bf 100644 --- a/tests/shared/utils/test_common.py +++ b/tests/shared/utils/test_common.py @@ -179,3 +179,87 @@ def test_class_from_module_path_fails(): module_path = "rasa.shared.core.domain.logger" with pytest.raises(RasaException): rasa.shared.utils.common.class_from_module_path(module_path) + + +def test_extract_duplicates(): + list_one = ["greet", {"inform": {"use_entities": []}}, "start_form", "goodbye"] + list_two = ["goodbye", {"inform": {"use_entities": ["destination"]}}] + + expected = ["goodbye", "inform"] + result = rasa.shared.utils.common.extract_duplicates(list_one, list_two) + + assert result == expected + + +def test_extract_duplicates_with_unique_lists(): + list_one = ["greet", {"inform": {"use_entities": []}}, "start_form", "goodbye"] + list_two = ["bot_challenge", {"mood_sad": {"ignore_entities": []}}] + + result = rasa.shared.utils.common.extract_duplicates(list_one, list_two) + assert result == [] + + +def test_clean_duplicates(): + duplicates = {"intents": ["goodbye", "inform"], "entities": []} + expected = {"intents": ["goodbye", "inform"]} + result = rasa.shared.utils.common.clean_duplicates(duplicates) + assert result == expected + + +def test_merge_lists(): + list_one = ["greet", "start_form", "goodbye"] + list_two = ["goodbye", "bot_challenge", "greet"] + expected = ["bot_challenge", "goodbye", "greet", "start_form"] + result = rasa.shared.utils.common.merge_lists(list_one, list_two) + + assert result == expected + + +@pytest.mark.parametrize("override_existing_values", [False, True]) +def test_merge_dicts(override_existing_values): + dict_1 = {"intents": ["greet", "goodbye"], "entities": ["name"]} + dict_2 = { + "responses": {"utter_greet": [{"text": "Hi"}]}, + "intents": ["bot_challenge"], + } + + if override_existing_values: + expected = { + "entities": ["name"], + "intents": ["bot_challenge"], + "responses": {"utter_greet": [{"text": "Hi"}]}, + } + else: + expected = { + "entities": ["name"], + "intents": ["greet", "goodbye"], + "responses": {"utter_greet": [{"text": "Hi"}]}, + } + + result = rasa.shared.utils.common.merge_dicts( + dict_1, dict_2, override_existing_values + ) + + assert result == expected + + +@pytest.mark.parametrize("override_existing_values", [False, True]) +def test_merge_lists_of_dicts(override_existing_values): + list_one = ["greet", {"inform": {"use_entities": []}}, "start_form", "goodbye"] + list_two = ["goodbye", {"inform": {"use_entities": ["destination"]}}] + + if override_existing_values: + expected = [ + "greet", + {"inform": {"use_entities": ["destination"]}}, + "start_form", + "goodbye", + ] + else: + expected = ["goodbye", {"inform": {"use_entities": []}}, "greet", "start_form"] + + result = rasa.shared.utils.common.merge_lists_of_dicts( + list_one, list_two, override_existing_values + ) + + assert result == expected diff --git a/tests/test_model_training.py b/tests/test_model_training.py index 3afc6d23da41..ad61a4dbd59c 100644 --- a/tests/test_model_training.py +++ b/tests/test_model_training.py @@ -911,8 +911,9 @@ def test_models_not_retrained_if_only_new_responses( utter_greet: - text: "Hi from Rasa" """ + domain_with_extra_response = Domain.from_yaml(domain_with_extra_response) - new_domain = domain.merge(Domain.from_yaml(domain_with_extra_response)) + new_domain = domain.merge(domain_with_extra_response) new_domain_path = tmp_path / "domain.yml" rasa.shared.utils.io.write_yaml(new_domain.as_dict(), new_domain_path) diff --git a/tests/test_server.py b/tests/test_server.py index aaedc74a5ca4..8a2966d7ee2f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1528,7 +1528,7 @@ async def test_unload_model_error(rasa_app: SanicASGITestClient): assert response.status == HTTPStatus.NO_CONTENT -async def test_get_domain(rasa_app: SanicASGITestClient): +async def test_get_domain(rasa_app: SanicASGITestClient, domain_path: Text): _, response = await rasa_app.get( "/domain", headers={"accept": rasa.server.JSON_CONTENT_TYPE} ) @@ -1536,12 +1536,10 @@ async def test_get_domain(rasa_app: SanicASGITestClient): content = response.json assert response.status == HTTPStatus.OK - assert "config" in content - assert "intents" in content - assert "entities" in content - assert "slots" in content - assert "responses" in content - assert "actions" in content + # assert only keys in `domain_path` fixture + original_domain_dict = Domain.load(domain_path).as_dict() + for key in original_domain_dict.keys(): + assert key in content async def test_get_domain_invalid_accept_header(rasa_app: SanicASGITestClient): diff --git a/tests/test_validator.py b/tests/test_validator.py index 80d57ffb4e53..6efae558467a 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,4 +1,4 @@ -from typing import Text, Any, Optional, List, Dict +from typing import Text import pytest from _pytest.logging import LogCaptureFixture @@ -7,7 +7,6 @@ from rasa.validator import Validator from rasa.shared.importers.rasa import RasaFileImporter -from rasa.shared.core.domain import Domain from pathlib import Path @@ -284,19 +283,28 @@ def test_early_exit_on_invalid_domain(): validator = Validator.from_importer(importer) validator.verify_domain_validity() - # two for non-unique domains, two for auto-fill removal + # two for non-unique domains, 2 for auto-fill removal assert len(record) == 4 - assert any( - [ - f"Loading domain from '{domain_path}' failed. Using empty domain. " - "Error: 'Intents are not unique! Found multiple intents with name(s) " - "['default', 'goodbye']. Either rename or remove the duplicate ones.'" - in warning.message.args[0] - for warning in record - ] + + non_unique_warnings = list( + filter( + lambda warning: f"Loading domain from '{domain_path}' failed. " + f"Using empty domain. Error: 'Intents are not unique! " + f"Found multiple intents with name(s) ['default', 'goodbye']. " + f"Either rename or remove the duplicate ones.'" in warning.message.args[0], + record, + ) + ) + assert len(non_unique_warnings) == 2 + + auto_fill_warnings = list( + filter( + lambda warning: "Slot auto-fill has been removed in 3.0" + in warning.message.args[0], + record, + ) ) - assert record[0].message.args[0] == record[2].message.args[0] - assert record[1].message.args[0] == record[3].message.args[0] + assert len(auto_fill_warnings) == 2 def test_verify_there_is_not_example_repetition_in_intents(): @@ -361,78 +369,6 @@ def test_verify_actions_in_rules_not_in_domain(tmp_path: Path, domain_path: Text ) -@pytest.mark.parametrize( - "duplicates,is_valid,warning_type,messages", - [ - (None, True, None, []), - ({}, True, None, []), - ({"responses": []}, True, None, []), - ( - {"responses": ["some_response"]}, - False, - UserWarning, - [ - "The following duplicated responses has been found across " - "multiple domain files: some_response" - ], - ), - ( - {"slots": ["some_slot"]}, - False, - UserWarning, - [ - "The following duplicated slots has been found across " - "multiple domain files: some_slot" - ], - ), - ( - {"forms": ["form1", "form2"]}, - False, - UserWarning, - [ - "The following duplicated forms has been found across " - "multiple domain files: form1, form2" - ], - ), - ( - {"forms": ["form1", "form2"], "slots": []}, - False, - UserWarning, - [ - "The following duplicated forms has been found across " - "multiple domain files: form1, form2" - ], - ), - ( - {"forms": ["form1", "form2"], "slots": ["slot1", "slot2", "slot3"]}, - False, - UserWarning, - [ - "The following duplicated forms has been found across " - "multiple domain files: form1, form2", - "The following duplicated slots has been found across " - "multiple domain files: slot1, slot2, slot3", - ], - ), - ], -) -def test_verify_domain_with_duplicates( - duplicates: Optional[Dict[Text, List[Text]]], - is_valid: bool, - warning_type: Any, - messages: List[Text], -): - domain = Domain([], [], [], {}, [], {}, duplicates=duplicates) - validator = Validator(domain, None, None, None) - - with pytest.warns(warning_type) as warning: - assert validator.verify_domain_duplicates() is is_valid - - assert len(warning) == len(messages) - for i in range(len(messages)): - assert messages[i] in warning[i].message.args[0] - - def test_verify_form_slots_invalid_domain(tmp_path: Path): domain = tmp_path / "domain.yml" domain.write_text( From 736d06fa198a553a06c0409341560c46f5ddf8aa Mon Sep 17 00:00:00 2001 From: Yiyao Wei Date: Mon, 28 Feb 2022 15:48:36 +0100 Subject: [PATCH 49/65] ci(chore): Update tf-cuda config for TF 2.7 (#10889) * Add config for TF 2.7 * Switch back to 11.2.0 for convenience * Add a step to warn user about tf-cude config * tmp: test * Revert "tmp: test" This reverts commit 11cfa764c01496d1480b0f768922cf57e97cc360. * tmp: test * Add step to the scheduled veresion * Remove , * Revert "tmp: test" This reverts commit 2ad744d731e268af11f4d04087419f17896dfcea. * Add step to notify slack * Use channel id, not webhook --- .github/configs/tf-cuda.json | 4 ++++ .../ci-model-regression-on-schedule.yml | 18 ++++++++++++++++++ .github/workflows/ci-model-regression.yml | 17 +++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/.github/configs/tf-cuda.json b/.github/configs/tf-cuda.json index bc5ddc487b81..4fdc2167ee19 100644 --- a/.github/configs/tf-cuda.json +++ b/.github/configs/tf-cuda.json @@ -12,6 +12,10 @@ { "TF": "2.6", "IMAGE_TAG": "cuda-11.2.0-cudnn8" + }, + { + "TF": "2.7", + "IMAGE_TAG": "cuda-11.2.0-cudnn8" } ] } diff --git a/.github/workflows/ci-model-regression-on-schedule.yml b/.github/workflows/ci-model-regression-on-schedule.yml index 3783f21300f3..b68f19308101 100644 --- a/.github/workflows/ci-model-regression-on-schedule.yml +++ b/.github/workflows/ci-model-regression-on-schedule.yml @@ -72,6 +72,23 @@ jobs: echo "GitHub runner image tag for TensorFlow ${{ env.TF_VERSION }} is ${GH_RUNNER_IMAGE_TAG}" echo GH_RUNNER_IMAGE_TAG=$GH_RUNNER_IMAGE_TAG >> $GITHUB_ENV + - name: Send warning if the current TF version does not have CUDA image tags configured + if: env.GH_RUNNER_IMAGE_TAG == 'latest' + env: + TF_CUDA_FILE: ./github/config/tf-cuda.json + run: |- + echo "::warning file=${TF_CUDA_FILE},line=3,col=1,endColumn=3::Missing cuda config for tf ${{ env.TF_VERSION }}. If you are not sure how to config CUDA, please reach out to infrastructure." + + - name: Notify slack on tf-cuda config updates + if: env.GH_RUNNER_IMAGE_TAG == 'latest' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + uses: voxmedia/github-action-slack-notify-build@212e9f7a9ca33368c8dd879d6053972128258985 + with: + channel_id: ${{ secrets.SLACK_ALERTS_CHANNEL_ID }} + status: WARNING + color: warning + - name: Render deployment template run: |- export GH_RUNNER_IMAGE_TAG=${{ env.GH_RUNNER_IMAGE_TAG }} @@ -104,6 +121,7 @@ jobs: status: FAILED color: danger + model_regression_test_gpu: name: Model Regression Tests - GPU continue-on-error: true diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index 2164c5096a8b..17d29f969208 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -160,6 +160,23 @@ jobs: echo "GitHub runner image tag for TensorFlow ${{ env.TF_VERSION }} is ${GH_RUNNER_IMAGE_TAG}" echo GH_RUNNER_IMAGE_TAG=$GH_RUNNER_IMAGE_TAG >> $GITHUB_ENV + - name: Send warning if the current TF version does not have CUDA image tags configured + if: env.GH_RUNNER_IMAGE_TAG == 'latest' + env: + TF_CUDA_FILE: ./github/config/tf-cuda.json + run: |- + echo "::warning file=${TF_CUDA_FILE},line=3,col=1,endColumn=3::Missing cuda config for tf ${{ env.TF_VERSION }}. If you are not sure how to config CUDA, please reach out to infrastructure." + + - name: Notify slack on tf-cuda config updates + if: env.GH_RUNNER_IMAGE_TAG == 'latest' + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + uses: voxmedia/github-action-slack-notify-build@212e9f7a9ca33368c8dd879d6053972128258985 + with: + channel_id: ${{ secrets.SLACK_ALERTS_CHANNEL_ID }} + status: WARNING + color: warning + - name: Render deployment template run: |- export GH_RUNNER_IMAGE_TAG=${{ env.GH_RUNNER_IMAGE_TAG }} From cf19a684c06ebc56bbf88284b2b15cbcf98358b9 Mon Sep 17 00:00:00 2001 From: Markus Hinsche Date: Tue, 1 Mar 2022 11:09:09 +0100 Subject: [PATCH 50/65] Regr test improve tags (#10945) Changes: - Add `host_name` name tag to agent metrics and custom metrics - Add `INDEX_REPETITION` to datadog agent tags - minor: Rename DATASET -> DATASET_NAME in start_dd_agent.sh --- .github/scripts/mr_publish_results.py | 1 + .github/scripts/start_dd_agent.sh | 4 +++- .github/tests/test_mr_publish_results.py | 1 + .../workflows/ci-model-regression-on-schedule.yml | 8 ++++++-- .github/workflows/ci-model-regression.yml | 14 +++++++++++--- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/scripts/mr_publish_results.py b/.github/scripts/mr_publish_results.py index da27a6bc0e18..a71d74300fa6 100644 --- a/.github/scripts/mr_publish_results.py +++ b/.github/scripts/mr_publish_results.py @@ -44,6 +44,7 @@ "accelerator_type": "ACCELERATOR_TYPE", "type": "TYPE", "index_repetition": "INDEX_REPETITION", + "host_name": "HOST_NAME", } GIT_RELATED_TAGS = { diff --git a/.github/scripts/start_dd_agent.sh b/.github/scripts/start_dd_agent.sh index dc28092a72db..8d2442cd0d92 100755 --- a/.github/scripts/start_dd_agent.sh +++ b/.github/scripts/start_dd_agent.sh @@ -15,7 +15,7 @@ sudo chmod 666 $DATADOG_YAML_PATH echo "tags:" echo "- service:rasa" echo "- accelerator_type:${ACCELERATOR_TYPE}" - echo "- dataset:${DATASET}" + echo "- dataset:${DATASET_NAME}" echo "- config:${CONFIG}" echo "- dataset_commit:${DATASET_COMMIT}" echo "- branch:${BRANCH}" @@ -30,6 +30,8 @@ sudo chmod 666 $DATADOG_YAML_PATH echo "- workflow:${GITHUB_WORKFLOW:-none}" echo "- github_run_id:${GITHUB_RUN_ID:-none}" echo "- github_event:${GITHUB_EVENT_NAME:-none}" + echo "- index_repetition:${INDEX_REPETITION}" + echo "- host_name:${HOST_NAME}" echo "" echo "apm_config:" echo " enabled: true" diff --git a/.github/tests/test_mr_publish_results.py b/.github/tests/test_mr_publish_results.py index fcd7e253ab30..c1c05cdd517e 100644 --- a/.github/tests/test_mr_publish_results.py +++ b/.github/tests/test_mr_publish_results.py @@ -35,6 +35,7 @@ "TOTAL_RUN_TIME": "5m58s", "TYPE": "nlu", "INDEX_REPETITION": "0", + "HOST_NAME": "github-runner-2223039222-22df222fcd-2cn7d", } diff --git a/.github/workflows/ci-model-regression-on-schedule.yml b/.github/workflows/ci-model-regression-on-schedule.yml index 025cdd963fd4..2fd75972fa7a 100644 --- a/.github/workflows/ci-model-regression-on-schedule.yml +++ b/.github/workflows/ci-model-regression-on-schedule.yml @@ -166,7 +166,7 @@ jobs: path: 'dataset' ref: "main" - - name: Set DATASET and CONFIG variables + - name: Set env variables id: set_dataset_config_vars env: DATASET_NAME: "${{ matrix.dataset }}" @@ -231,6 +231,9 @@ jobs: echo "TEST_DIR=${TEST_DIR}" >> $GITHUB_ENV fi + HOST_NAME=`hostname` + echo "HOST_NAME=${HOST_NAME}" >> $GITHUB_ENV + - name: Checkout dataset - external uses: actions/checkout@v2 if: steps.set_dataset_config_vars.outputs.is_external == 'true' @@ -251,13 +254,14 @@ jobs: - name: Start Datadog Agent if: steps.set_dataset_config_vars.outputs.is_dataset_exists == 'true' && steps.set_dataset_config_vars.outputs.is_config_exists == 'true' env: - DATASET: "${{ matrix.dataset }}" + DATASET_NAME: "${{ matrix.dataset }}" CONFIG: "${{ matrix.config }}" DATASET_COMMIT: "${{ steps.set-dataset-commit.outputs.dataset_commit }}" BRANCH: ${{ github.ref }} GITHUB_SHA: "${{ github.sha }}" TYPE: "${{ matrix.type }}" DATASET_REPOSITORY_BRANCH: "main" + INDEX_REPETITION: "${{ matrix.index_repetition }}" run: | .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" ${{ env.NVML_INTERVAL_IN_SEC }} diff --git a/.github/workflows/ci-model-regression.yml b/.github/workflows/ci-model-regression.yml index fbf107abfce8..3cd7528e8f49 100644 --- a/.github/workflows/ci-model-regression.yml +++ b/.github/workflows/ci-model-regression.yml @@ -327,6 +327,9 @@ jobs: echo "TEST_DIR=${TEST_DIR}" >> $GITHUB_ENV fi + HOST_NAME=`hostname` + echo "HOST_NAME=${HOST_NAME}" >> $GITHUB_ENV + - name: Checkout dataset - external uses: actions/checkout@v2 if: steps.set_dataset_config_vars.outputs.is_external == 'true' @@ -347,7 +350,7 @@ jobs: - name: Start Datadog Agent if: steps.set_dataset_config_vars.outputs.is_dataset_exists == 'true' && steps.set_dataset_config_vars.outputs.is_config_exists == 'true' env: - DATASET: "${{ matrix.dataset }}" + DATASET_NAME: "${{ matrix.dataset }}" CONFIG: "${{ matrix.config }}" DATASET_COMMIT: "${{ steps.set-dataset-commit.outputs.dataset_commit }}" BRANCH: ${{ github.head_ref }} @@ -355,6 +358,7 @@ jobs: PR_ID: "${{ github.event.number }}" TYPE: "${{ matrix.type }}" DATASET_REPOSITORY_BRANCH: ${{ needs.read_test_configuration.outputs.dataset_branch }} + INDEX_REPETITION: "${{ matrix.index_repetition }}" run: | export PR_URL="https://github.com/${GITHUB_REPOSITORY}/pull/${{ github.event.number }}" .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" ${{ env.NVML_INTERVAL_IN_SEC }} @@ -509,7 +513,7 @@ jobs: sudo curl -o /usr/local/bin/gomplate -sSL https://github.com/hairyhenderson/gomplate/releases/download/v3.9.0/gomplate_linux-amd64 sudo chmod +x /usr/local/bin/gomplate - - name: Set DATASET and CONFIG variables + - name: Set env variables id: set_dataset_config_vars env: DATASET_NAME: "${{ matrix.dataset }}" @@ -568,6 +572,9 @@ jobs: echo "TEST_DIR=${TEST_DIR}" >> $GITHUB_ENV fi + HOST_NAME=`hostname` + echo "HOST_NAME=${HOST_NAME}" >> $GITHUB_ENV + - name: Checkout dataset - external uses: actions/checkout@v2 if: steps.set_dataset_config_vars.outputs.is_external == 'true' @@ -588,7 +595,7 @@ jobs: - name: Start Datadog Agent if: steps.set_dataset_config_vars.outputs.is_dataset_exists == 'true' && steps.set_dataset_config_vars.outputs.is_config_exists == 'true' env: - DATASET: "${{ matrix.dataset }}" + DATASET_NAME: "${{ matrix.dataset }}" CONFIG: "${{ matrix.config }}" DATASET_COMMIT: "${{ steps.set-dataset-commit.outputs.dataset_commit }}" BRANCH: ${{ github.head_ref }} @@ -596,6 +603,7 @@ jobs: PR_ID: "${{ github.event.number }}" TYPE: "${{ matrix.type }}" DATASET_REPOSITORY_BRANCH: ${{ matrix.dataset_branch }} + INDEX_REPETITION: "${{ matrix.index_repetition }}" run: | export PR_URL="https://github.com/${GITHUB_REPOSITORY}/pull/${{ github.event.number }}" .github/scripts/start_dd_agent.sh "${{ secrets.DD_API_KEY }}" "${{ env.ACCELERATOR_TYPE }}" ${{ env.NVML_INTERVAL_IN_SEC }} From 029d01d85a0a4a885613f4fd3b8b4b8d96af306a Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 11:35:21 +0100 Subject: [PATCH 51/65] enable mypy "var-annotated" check --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index d8d9a24dd75b..38827fec2c07 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,5 +47,4 @@ disallow_untyped_decorators = True # FIXME: working our way towards removing these # see https://github.com/RasaHQ/rasa/pull/6470 # the list below is sorted by the number of errors for each error code, in decreasing order -disable_error_code = arg-type, assignment, var-annotated, union-attr, - override, misc +disable_error_code = arg-type, assignment, union-attr, override, misc From 9fc462da870f69f9976be3bc081675844b9f64c2 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 13:43:21 +0100 Subject: [PATCH 52/65] fix type annotation in rasa.engine --- rasa/engine/graph.py | 2 +- rasa/engine/storage/local_model_storage.py | 6 +++--- rasa/engine/storage/storage.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rasa/engine/graph.py b/rasa/engine/graph.py index 0ad6bf751d7d..f1ab0d62b9e6 100644 --- a/rasa/engine/graph.py +++ b/rasa/engine/graph.py @@ -75,7 +75,7 @@ def as_dict(self) -> Dict[Text, Any]: Returns: The graph schema in a format which can be dumped as JSON or other formats. """ - serializable_graph_schema = {"nodes": {}} + serializable_graph_schema: Dict[Text, Dict[Text, Any]] = {"nodes": {}} for node_name, node in self.nodes.items(): serializable = dataclasses.asdict(node) diff --git a/rasa/engine/storage/local_model_storage.py b/rasa/engine/storage/local_model_storage.py index 84c2d48d3dfd..a4b80c1504ca 100644 --- a/rasa/engine/storage/local_model_storage.py +++ b/rasa/engine/storage/local_model_storage.py @@ -8,7 +8,7 @@ from contextlib import contextmanager from datetime import datetime from pathlib import Path -from typing import Text, ContextManager, Tuple, Union +from typing import Text, Generator, Tuple, Union import rasa.utils.common import rasa.shared.utils.io @@ -112,7 +112,7 @@ def _load_metadata(directory: Path) -> ModelMetadata: return ModelMetadata.from_dict(serialized_metadata) @contextmanager - def write_to(self, resource: Resource) -> ContextManager[Path]: + def write_to(self, resource: Resource) -> Generator[Path, None, None]: """Persists data for a resource (see parent class for full docstring).""" logger.debug(f"Resource '{resource.name}' was requested for writing.") directory = self._directory_for_resource(resource) @@ -128,7 +128,7 @@ def _directory_for_resource(self, resource: Resource) -> Path: return self._storage_path / resource.name @contextmanager - def read_from(self, resource: Resource) -> ContextManager[Path]: + def read_from(self, resource: Resource) -> Generator[Path, None, None]: """Provides the data of a `Resource` (see parent class for full docstring).""" logger.debug(f"Resource '{resource.name}' was requested for reading.") directory = self._directory_for_resource(resource) diff --git a/rasa/engine/storage/storage.py b/rasa/engine/storage/storage.py index a4740ce8898c..8c5d5b48baa2 100644 --- a/rasa/engine/storage/storage.py +++ b/rasa/engine/storage/storage.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Tuple, Union, Text, ContextManager, Dict, Any, Optional +from typing import Tuple, Union, Text, Generator, Dict, Any, Optional from packaging import version from rasa.constants import MINIMUM_COMPATIBLE_VERSION @@ -74,7 +74,7 @@ def metadata_from_archive( @contextmanager @abc.abstractmethod - def write_to(self, resource: Resource) -> ContextManager[Path]: + def write_to(self, resource: Resource) -> Generator[Path, None, None]: """Persists data for a given resource. This `Resource` can then be accessed in dependent graph nodes via @@ -90,7 +90,7 @@ def write_to(self, resource: Resource) -> ContextManager[Path]: @contextmanager @abc.abstractmethod - def read_from(self, resource: Resource) -> ContextManager[Path]: + def read_from(self, resource: Resource) -> Generator[Path, None, None]: """Provides the data of a persisted `Resource`. Args: From b1c87099c7f13cbd72e4c10ba6b3f117b167363a Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 13:43:45 +0100 Subject: [PATCH 53/65] add type annotations in rasa.nlu --- rasa/nlu/classifiers/diet_classifier.py | 2 +- rasa/nlu/emulators/luis.py | 2 +- rasa/nlu/emulators/wit.py | 2 +- rasa/nlu/extractors/crf_entity_extractor.py | 2 +- .../sparse_featurizer/count_vectors_featurizer.py | 6 ++++-- rasa/nlu/selectors/response_selector.py | 2 +- rasa/nlu/test.py | 4 ++-- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/rasa/nlu/classifiers/diet_classifier.py b/rasa/nlu/classifiers/diet_classifier.py index 790802f07745..49e7f0d580ce 100644 --- a/rasa/nlu/classifiers/diet_classifier.py +++ b/rasa/nlu/classifiers/diet_classifier.py @@ -946,7 +946,7 @@ def _predict_label( ) -> Tuple[Dict[Text, Any], List[Dict[Text, Any]]]: """Predicts the intent of the provided message.""" label: Dict[Text, Any] = {"name": None, "confidence": 0.0} - label_ranking = [] + label_ranking: List[Dict[Text, Any]] = [] if predict_out is None: return label, label_ranking diff --git a/rasa/nlu/emulators/luis.py b/rasa/nlu/emulators/luis.py index 97192164689a..b2653548cb65 100644 --- a/rasa/nlu/emulators/luis.py +++ b/rasa/nlu/emulators/luis.py @@ -43,7 +43,7 @@ def _entities( if ENTITIES not in data: return {} - entities = {"$instance": {}} + entities: Dict[Text, Dict[Text, List[Dict[Text, Any]]]] = {"$instance": {}} for e in data[ENTITIES]: # LUIS API v3 uses entity roles instead of entity names # (it's possible because its roles are unique): diff --git a/rasa/nlu/emulators/wit.py b/rasa/nlu/emulators/wit.py index bdbc80589684..1f5c9c0d89e8 100644 --- a/rasa/nlu/emulators/wit.py +++ b/rasa/nlu/emulators/wit.py @@ -35,7 +35,7 @@ def normalise_response_json(self, data: Dict[Text, Any]) -> Dict[Text, Any]: entity_name = entity[ENTITY_ATTRIBUTE_TYPE] role = entity.get(ENTITY_ATTRIBUTE_ROLE, entity_name) entity_name_including_role = f"{entity[ENTITY_ATTRIBUTE_TYPE]}:{role}" - normalized_entity = { + normalized_entity: Dict[Text, Any] = { "confidence": entity.get("confidence_entity") or 1, "name": entity_name, "value": entity[ENTITY_ATTRIBUTE_VALUE], diff --git a/rasa/nlu/extractors/crf_entity_extractor.py b/rasa/nlu/extractors/crf_entity_extractor.py index fc6f5bcf65d3..1332c250d55a 100644 --- a/rasa/nlu/extractors/crf_entity_extractor.py +++ b/rasa/nlu/extractors/crf_entity_extractor.py @@ -292,7 +292,7 @@ def extract_entities(self, message: Message) -> List[Dict[Text, Any]]: tokens = message.get(TOKENS_NAMES[TEXT]) crf_tokens = self._convert_to_crf_tokens(message) - predictions = {} + predictions: Dict[Text, List[Dict[Text, float]]] = {} for tag_name, entity_tagger in self.entity_taggers.items(): # use predicted entity tags as features for second level CRFs include_tag_features = tag_name != ENTITY_ATTRIBUTE_TYPE diff --git a/rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py b/rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py index 945851d31c85..221f1bf260f6 100644 --- a/rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py +++ b/rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py @@ -44,6 +44,8 @@ class CountVectorsFeaturizer(SparseFeaturizer, GraphComponent): from https://arxiv.org/abs/1810.07150. """ + OOV_words: List[Text] + @classmethod def required_components(cls) -> List[Type]: """Components that should be included in the pipeline before this component.""" @@ -551,8 +553,8 @@ def _create_features( if not self.vectorizers.get(attribute): return [None], [None] - sequence_features = [] - sentence_features = [] + sequence_features: List[Optional[scipy.sparse.spmatrix]] = [] + sentence_features: List[Optional[scipy.sparse.spmatrix]] = [] for i, tokens in enumerate(all_tokens): if not tokens: diff --git a/rasa/nlu/selectors/response_selector.py b/rasa/nlu/selectors/response_selector.py index a68a7278792a..f3ba95fdd08c 100644 --- a/rasa/nlu/selectors/response_selector.py +++ b/rasa/nlu/selectors/response_selector.py @@ -357,7 +357,7 @@ def _warn_about_transformer_and_hidden_layers_enabled( self.component_config[HIDDEN_LAYERS_SIZES] == default_config[HIDDEN_LAYERS_SIZES] ) - config_for_disabling_hidden_layers = { + config_for_disabling_hidden_layers: Dict[Text, List[Any]] = { k: [] for k, _ in default_config[HIDDEN_LAYERS_SIZES].items() } # warn if the hidden layers aren't disabled diff --git a/rasa/nlu/test.py b/rasa/nlu/test.py index 28b82d9993f1..3f2fefaf063a 100644 --- a/rasa/nlu/test.py +++ b/rasa/nlu/test.py @@ -1730,7 +1730,7 @@ async def compute_metrics( response_selection_results ) - intent_metrics = {} + intent_metrics: IntentMetrics = {} if intent_results: intent_metrics = _compute_metrics( intent_results, "intent_target", "intent_prediction" @@ -1740,7 +1740,7 @@ async def compute_metrics( if entity_results: entity_metrics = _compute_entity_metrics(entity_results) - response_selection_metrics = {} + response_selection_metrics: ResponseSelectionMetrics = {} if response_selection_results: response_selection_metrics = _compute_metrics( response_selection_results, From 11d4a71cf78a1c75d05cabafe14fd72c0f470945 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 13:44:04 +0100 Subject: [PATCH 54/65] add type annotations in rasa.utils.tensorflow --- rasa/utils/tensorflow/data_generator.py | 2 +- rasa/utils/tensorflow/model_data.py | 18 ++++++++++++------ rasa/utils/tensorflow/model_data_utils.py | 2 +- rasa/utils/tensorflow/models.py | 2 +- rasa/utils/tensorflow/rasa_layers.py | 4 ++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/rasa/utils/tensorflow/data_generator.py b/rasa/utils/tensorflow/data_generator.py index d4d4476858dc..c9a9f8d235f1 100644 --- a/rasa/utils/tensorflow/data_generator.py +++ b/rasa/utils/tensorflow/data_generator.py @@ -367,7 +367,7 @@ def __init__( # actual batch size will be set inside `on_epoch_end` self._current_batch_size = 0 # create separate data variable that will store modified data for each batch - self._data = {} + self._data: Data = {} self.on_epoch_end() def __len__(self) -> int: diff --git a/rasa/utils/tensorflow/model_data.py b/rasa/utils/tensorflow/model_data.py index 7167ca86156d..99a71ae43319 100644 --- a/rasa/utils/tensorflow/model_data.py +++ b/rasa/utils/tensorflow/model_data.py @@ -254,7 +254,7 @@ def __init__( self.label_sub_key = label_sub_key # should be updated when features are added self.num_examples = self.number_of_examples() - self.sparse_feature_sizes = {} + self.sparse_feature_sizes: Dict[Text, Dict[Text, List[int]]] = {} def get( self, key: Text, sub_key: Optional[Text] = None @@ -321,7 +321,7 @@ def first_data_example(self) -> Data: Returns: The simplified data. """ - out_data = {} + out_data: Data = {} for key, attribute_data in self.data.items(): out_data[key] = {} for sub_key, features in attribute_data.items(): @@ -569,7 +569,7 @@ def split( for data in attribute_data.values() for v in data ] - solo_values = [ + solo_values: List[Any] = [ [] for attribute_data in self.data.values() for data in attribute_data.values() @@ -695,7 +695,9 @@ def balanced_data(self, data: Data, batch_size: int, shuffle: bool) -> Data: # if a label was skipped in current batch skipped = [False] * num_label_ids - new_data = defaultdict(lambda: defaultdict(list)) + new_data: Dict[Text, Dict[Text, List[List[FeatureArray]]]] = defaultdict( + lambda: defaultdict(list) + ) while min(num_data_cycles) == 0: if shuffle: @@ -846,8 +848,12 @@ def _convert_train_test_split( Returns: The test and train RasaModelData """ - data_train = defaultdict(lambda: defaultdict(list)) - data_val = defaultdict(lambda: defaultdict(list)) + data_train: Dict[Text, Dict[Text, List[FeatureArray]]] = defaultdict( + lambda: defaultdict(list) + ) + data_val: Dict[Text, Dict[Text, List[Any]]] = defaultdict( + lambda: defaultdict(list) + ) # output_values = x_train, x_val, y_train, y_val, z_train, z_val, etc. # order is kept, e.g. same order as model data keys diff --git a/rasa/utils/tensorflow/model_data_utils.py b/rasa/utils/tensorflow/model_data_utils.py index cba475d8346a..694d87f52eae 100644 --- a/rasa/utils/tensorflow/model_data_utils.py +++ b/rasa/utils/tensorflow/model_data_utils.py @@ -52,7 +52,7 @@ def featurize_training_examples( output = [] for example in training_examples: - attribute_to_features = {} + attribute_to_features: Dict[Text, List["Features"]] = {} for attribute in attributes: if attribute == ENTITIES: attribute_to_features[attribute] = [] diff --git a/rasa/utils/tensorflow/models.py b/rasa/utils/tensorflow/models.py index 43856112d3b5..6a09bd343ca3 100644 --- a/rasa/utils/tensorflow/models.py +++ b/rasa/utils/tensorflow/models.py @@ -295,7 +295,7 @@ def run_inference( Returns: Model outputs corresponding to the inputs fed. """ - outputs = {} + outputs: Dict[Text, Union[np.ndarray, Dict[Text, Any]]] = {} (data_generator, _) = rasa.utils.train_utils.create_data_generators( model_data=model_data, batch_sizes=batch_size, epochs=1, shuffle=False ) diff --git a/rasa/utils/tensorflow/rasa_layers.py b/rasa/utils/tensorflow/rasa_layers.py index 8b68eceaa167..9b3d0a89b5a6 100644 --- a/rasa/utils/tensorflow/rasa_layers.py +++ b/rasa/utils/tensorflow/rasa_layers.py @@ -233,7 +233,7 @@ def __init__( ) # Prepare dropout and sparse-to-dense layers if any sparse tensors are expected - self._tf_layers = {} + self._tf_layers: Dict[Text, tf.keras.layers.Layer] = {} if any([signature.is_sparse for signature in feature_type_signature]): self._prepare_layers_for_sparse_tensors(attribute, feature_type, config) @@ -404,7 +404,7 @@ def __init__( super().__init__(name=f"rasa_feature_combining_layer_{attribute}") - self._tf_layers = {} + self._tf_layers: Dict[Text, tf.keras.layers.Layer] = {} # Prepare sparse-dense combining layers for each present feature type self._feature_types_present = self._get_present_feature_types( From 2dcb435dad073fb26a6481d14b40a44346b1d64e Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 16:17:58 +0100 Subject: [PATCH 55/65] add type annotations in rasa.core --- rasa/core/actions/action.py | 2 +- rasa/core/actions/forms.py | 6 ++--- rasa/core/channels/channel.py | 2 +- rasa/core/channels/hangouts.py | 2 +- rasa/core/channels/rest.py | 2 +- rasa/core/evaluation/marker_base.py | 2 +- rasa/core/evaluation/marker_stats.py | 2 +- .../featurizers/single_state_featurizer.py | 6 ++--- rasa/core/featurizers/tracker_featurizers.py | 2 +- rasa/core/lock_store.py | 4 +-- rasa/core/migrate.py | 6 ++--- rasa/core/policies/memoization.py | 2 +- rasa/core/policies/rule_policy.py | 25 +++++++++++-------- .../core/policies/unexpected_intent_policy.py | 2 +- rasa/core/processor.py | 2 +- rasa/core/tracker_store.py | 2 +- rasa/core/training/interactive.py | 6 ++--- rasa/core/training/story_conflict.py | 4 +-- rasa/core/training/training.py | 2 +- rasa/model_testing.py | 2 +- rasa/nlu/test.py | 2 +- rasa/server.py | 2 +- 22 files changed, 46 insertions(+), 41 deletions(-) diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index fcf621d10db2..d6277b164ce4 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -1159,7 +1159,7 @@ async def run( ) -> List[Event]: """Runs action. Please see parent class for the full docstring.""" slot_events: List[Event] = [] - executed_custom_actions = set() + executed_custom_actions: Set[Text] = set() user_slots = [ slot for slot in domain.slots if slot.name not in DEFAULT_SLOT_NAMES diff --git a/rasa/core/actions/forms.py b/rasa/core/actions/forms.py index 0c68086ccc94..a66d52a3b16e 100644 --- a/rasa/core/actions/forms.py +++ b/rasa/core/actions/forms.py @@ -147,8 +147,8 @@ def _create_unique_entity_mappings(self, domain: Domain) -> Set[Text]: Returns: A set of json dumps of unique mappings of type `from_entity`. """ - unique_entity_slot_mappings = set() - duplicate_entity_slot_mappings = set() + unique_entity_slot_mappings: Set[Text] = set() + duplicate_entity_slot_mappings: Set[Text] = set() domain_slots = domain.as_dict().get(KEY_SLOTS) for slot in domain.required_slots_for_form(self.name()): for slot_mapping in domain_slots.get(slot).get(SLOT_MAPPINGS): @@ -360,7 +360,7 @@ def _get_slot_extractions( events_since_last_user_uttered = FormAction._get_events_since_last_user_uttered( tracker ) - slot_values = {} + slot_values: Dict[Text, Any] = {} required_slots = self._add_dynamic_slots_requested_by_dynamic_forms( tracker, domain diff --git a/rasa/core/channels/channel.py b/rasa/core/channels/channel.py index 5fca3ad9868e..886dc45dc484 100644 --- a/rasa/core/channels/channel.py +++ b/rasa/core/channels/channel.py @@ -330,7 +330,7 @@ class CollectingOutputChannel(OutputChannel): (doesn't send them anywhere, just collects them).""" def __init__(self) -> None: - self.messages = [] + self.messages: List[Dict[Text, Any]] = [] @classmethod def name(cls) -> Text: diff --git a/rasa/core/channels/hangouts.py b/rasa/core/channels/hangouts.py index 1a7c19a96867..2dcf921fe012 100644 --- a/rasa/core/channels/hangouts.py +++ b/rasa/core/channels/hangouts.py @@ -33,7 +33,7 @@ def name(cls) -> Text: def __init__(self) -> None: """Starts messages as empty dictionary.""" - self.messages = {} + self.messages: Dict[Text, Any] = {} @staticmethod def _text_card(message: Dict[Text, Any]) -> Dict: diff --git a/rasa/core/channels/rest.py b/rasa/core/channels/rest.py index 0cd8c04c0f7b..649f1738b02c 100644 --- a/rasa/core/channels/rest.py +++ b/rasa/core/channels/rest.py @@ -67,7 +67,7 @@ def stream_response( metadata: Optional[Dict[Text, Any]], ) -> Callable[[Any], Awaitable[None]]: async def stream(resp: Any) -> None: - q = Queue() + q: Queue = Queue() task = asyncio.ensure_future( self.on_message_wrapper( on_new_message, text, q, sender_id, input_channel, metadata diff --git a/rasa/core/evaluation/marker_base.py b/rasa/core/evaluation/marker_base.py index b2d680846c45..6f2cb806dcfb 100644 --- a/rasa/core/evaluation/marker_base.py +++ b/rasa/core/evaluation/marker_base.py @@ -489,7 +489,7 @@ def _collect_yaml_files_from_path(path: Union[Text, Path]) -> List[Text]: @staticmethod def _collect_configs_from_yaml_files(yaml_files: List[Text]) -> Dict[Text, Dict]: - marker_names = set() + marker_names: Set[Text] = set() loaded_configs: Dict[Text, Dict] = {} for yaml_file in yaml_files: loaded_config = rasa.shared.utils.io.read_yaml_file(yaml_file) diff --git a/rasa/core/evaluation/marker_stats.py b/rasa/core/evaluation/marker_stats.py index 2812a0a811ab..737263fd524c 100644 --- a/rasa/core/evaluation/marker_stats.py +++ b/rasa/core/evaluation/marker_stats.py @@ -72,7 +72,7 @@ def _add_num_user_turns_str_to(stat_name: Text) -> Text: def __init__(self) -> None: """Creates a new marker statistics object.""" # to ensure consistency of processed rows - self._marker_names = [] + self._marker_names: List[Text] = [] # (1) For collecting the per-session analysis: # NOTE: we could stream / compute them later instead of collecting them... diff --git a/rasa/core/featurizers/single_state_featurizer.py b/rasa/core/featurizers/single_state_featurizer.py index c9e657aed473..5c4615c269b8 100644 --- a/rasa/core/featurizers/single_state_featurizer.py +++ b/rasa/core/featurizers/single_state_featurizer.py @@ -39,9 +39,9 @@ class SingleStateFeaturizer: def __init__(self) -> None: """Initialize the single state featurizer.""" - self._default_feature_states = {} - self.action_texts = [] - self.entity_tag_specs = [] + self._default_feature_states: Dict[Text, Any] = {} + self.action_texts: List[Text] = [] + self.entity_tag_specs: List[EntityTagSpec] = [] def _create_entity_tag_specs( self, bilou_tagging: bool = False diff --git a/rasa/core/featurizers/tracker_featurizers.py b/rasa/core/featurizers/tracker_featurizers.py index 1abb310f4de3..e90154be3eff 100644 --- a/rasa/core/featurizers/tracker_featurizers.py +++ b/rasa/core/featurizers/tracker_featurizers.py @@ -1056,7 +1056,7 @@ def _extract_examples( tracker_states[:label_index], self.max_history ) label = [event.intent_name or event.text] - entities = [{}] + entities: List[Dict[Text, Any]] = [{}] yield sliced_states, label, entities diff --git a/rasa/core/lock_store.py b/rasa/core/lock_store.py index ac78da3df30f..39d685ec711c 100644 --- a/rasa/core/lock_store.py +++ b/rasa/core/lock_store.py @@ -4,7 +4,7 @@ import os from async_generator import asynccontextmanager -from typing import Text, Union, Optional, AsyncGenerator +from typing import AsyncGenerator, Dict, Optional, Text, Union from rasa.shared.exceptions import RasaException, ConnectionException import rasa.shared.utils.common @@ -274,7 +274,7 @@ class InMemoryLockStore(LockStore): """In-memory store for ticket locks.""" def __init__(self) -> None: - self.conversation_locks = {} + self.conversation_locks: Dict[Text, TicketLock] = {} super().__init__() def get_lock(self, conversation_id: Text) -> Optional[TicketLock]: diff --git a/rasa/core/migrate.py b/rasa/core/migrate.py index 8de15d0fb63b..cb24d9e9bef8 100644 --- a/rasa/core/migrate.py +++ b/rasa/core/migrate.py @@ -210,9 +210,9 @@ def _migrate_domain_files( backup_location: where to backup all domain files out_path: location where to store the migrated files """ - slots = {} - forms = {} - entities = [] + slots: Dict[Text, Any] = {} + forms: Dict[Text, Any] = {} + entities: List[Any] = [] domain_files = [ file for file in domain_path.iterdir() if Domain.is_domain_file(file) diff --git a/rasa/core/policies/memoization.py b/rasa/core/policies/memoization.py index 405578e25f52..46eb640cac8d 100644 --- a/rasa/core/policies/memoization.py +++ b/rasa/core/policies/memoization.py @@ -104,7 +104,7 @@ def _create_lookup_from_states( Returns: lookup dictionary """ - lookup = {} + lookup: Dict[Text, Text] = {} if not trackers_as_states: return lookup diff --git a/rasa/core/policies/rule_policy.py b/rasa/core/policies/rule_policy.py index b16884baa5c2..702f13ecb1ea 100644 --- a/rasa/core/policies/rule_policy.py +++ b/rasa/core/policies/rule_policy.py @@ -159,7 +159,7 @@ def __init__( self._enable_fallback_prediction = config["enable_fallback_prediction"] self._check_for_contradictions = config["check_for_contradictions"] - self._rules_sources = defaultdict(list) + self._rules_sources: Dict[Text, List[Tuple[Text, Text]]] = defaultdict(list) @classmethod def raise_if_incompatible_with_domain( @@ -190,7 +190,7 @@ def _is_rule_snippet_state(state: State) -> bool: return prev_action_name == RULE_SNIPPET_ACTION_NAME def _create_feature_key(self, states: List[State]) -> Optional[Text]: - new_states = [] + new_states: List[State] = [] for state in reversed(states): if self._is_rule_snippet_state(state): # remove all states before RULE_SNIPPET_ACTION_NAME @@ -493,7 +493,7 @@ def _collect_sources( tracker: TrackerWithCachedStates, predicted_action_name: Optional[Text], gold_action_name: Text, - prediction_source: Optional[Text], + prediction_source: Text, ) -> None: # we need to remember which action should be predicted by the rule # in order to correctly output the names of the contradicting rules @@ -564,7 +564,11 @@ def _check_prediction( gold_action_name: Text, prediction_source: Optional[Text], ) -> List[Text]: - if not predicted_action_name or predicted_action_name == gold_action_name: + if ( + not predicted_action_name + or not prediction_source + or predicted_action_name == gold_action_name + ): return [] if self._should_delete(prediction_source, tracker, predicted_action_name): @@ -636,12 +640,13 @@ def _run_prediction_on_trackers( running_tracker, domain, gold_action_name ) if collect_sources: - self._collect_sources( - running_tracker, - predicted_action_name, - gold_action_name, - prediction_source, - ) + if prediction_source: + self._collect_sources( + running_tracker, + predicted_action_name, + gold_action_name, + prediction_source, + ) else: # to be able to remove only rules turns from the dialogue history # for ML policies, diff --git a/rasa/core/policies/unexpected_intent_policy.py b/rasa/core/policies/unexpected_intent_policy.py index eae372ade64e..9bab29063e44 100644 --- a/rasa/core/policies/unexpected_intent_policy.py +++ b/rasa/core/policies/unexpected_intent_policy.py @@ -771,7 +771,7 @@ def _collect_label_id_grouped_scores( if LABEL_PAD_ID in unique_label_ids: unique_label_ids.remove(LABEL_PAD_ID) - label_id_scores = { + label_id_scores: Dict[int, Dict[Text, List[float]]] = { label_id: {POSITIVE_SCORES_KEY: [], NEGATIVE_SCORES_KEY: []} for label_id in unique_label_ids } diff --git a/rasa/core/processor.py b/rasa/core/processor.py index 38518141f75c..b89a3691a05f 100644 --- a/rasa/core/processor.py +++ b/rasa/core/processor.py @@ -511,7 +511,7 @@ async def handle_reminder( ) else: intent = reminder_event.intent - entities = reminder_event.entities or {} + entities: Union[List[Dict], Dict] = reminder_event.entities or {} await self.trigger_external_user_uttered( intent, entities, tracker, output_channel ) diff --git a/rasa/core/tracker_store.py b/rasa/core/tracker_store.py index 54f3d06b4173..1b4ed2324d34 100644 --- a/rasa/core/tracker_store.py +++ b/rasa/core/tracker_store.py @@ -273,7 +273,7 @@ def __init__( event_broker: Optional[EventBroker] = None, **kwargs: Dict[Text, Any], ) -> None: - self.store = {} + self.store: Dict[Text, Text] = {} super().__init__(domain, event_broker, **kwargs) def save(self, tracker: DialogueStateTracker) -> None: diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py index 477a9cf4c84a..504272c0cd22 100644 --- a/rasa/core/training/interactive.py +++ b/rasa/core/training/interactive.py @@ -114,7 +114,7 @@ OTHER_ACTION = uuid.uuid4().hex NEW_ACTION = uuid.uuid4().hex -NEW_RESPONSES = {} +NEW_RESPONSES: Dict[Text, List[Dict[Text, Any]]] = {} MAX_NUMBER_OF_TRAINING_STORIES_FOR_VISUALIZATION = 200 @@ -320,7 +320,7 @@ async def _ask_questions( """Ask the user a question, if Ctrl-C is pressed provide user with menu.""" should_retry = True - answers = {} + answers: Any = {} while should_retry: answers = questions.ask() @@ -923,7 +923,7 @@ def _write_domain_to_file( messages = _collect_messages(events) actions = _collect_actions(events) - responses = NEW_RESPONSES # type: Dict[Text, List[Dict[Text, Any]]] + responses = NEW_RESPONSES # TODO for now there is no way to distinguish between action and form collected_actions = list( diff --git a/rasa/core/training/story_conflict.py b/rasa/core/training/story_conflict.py index dc43a111a7e5..281058ebbe96 100644 --- a/rasa/core/training/story_conflict.py +++ b/rasa/core/training/story_conflict.py @@ -38,7 +38,7 @@ def __init__(self, sliced_states: List[State]) -> None: self._sliced_states = sliced_states # A list of actions that all follow from the same state. - self._conflicting_actions = defaultdict( + self._conflicting_actions: Dict[Text, List[Text]] = defaultdict( list ) # {"action": ["story_1", ...], ...} @@ -196,7 +196,7 @@ def _find_conflicting_states( """ # Create a 'state -> list of actions' dict, where the state is # represented by its hash - state_action_mapping = defaultdict(list) + state_action_mapping: Dict[int, List[int]] = defaultdict(list) for element in _sliced_states_iterator(trackers, domain, max_history, tokenizer): hashed_state = element.sliced_states_hash diff --git a/rasa/core/training/training.py b/rasa/core/training/training.py index dde083940857..16037e6fb2e7 100644 --- a/rasa/core/training/training.py +++ b/rasa/core/training/training.py @@ -65,7 +65,7 @@ def create_action_fingerprints( # take into account only featurized slots featurized_slots = {slot.name for slot in domain.slots if slot.has_features()} - action_fingerprints = defaultdict(dict) + action_fingerprints: Dict[Text, Dict[Text, List[Text]]] = defaultdict(dict) for action_name, events_after_action in events_after_actions.items(): slots = list( set( diff --git a/rasa/model_testing.py b/rasa/model_testing.py index b9a6fb9334c9..3e66ae43769f 100644 --- a/rasa/model_testing.py +++ b/rasa/model_testing.py @@ -244,7 +244,7 @@ async def compare_nlu_models( bases = [os.path.basename(nlu_config) for nlu_config in configs] model_names = [os.path.splitext(base)[0] for base in bases] - f1_score_results = { + f1_score_results: Dict[Text, List[List[float]]] = { model_name: [[] for _ in range(runs)] for model_name in model_names } diff --git a/rasa/nlu/test.py b/rasa/nlu/test.py index 3f2fefaf063a..34aa05306a10 100644 --- a/rasa/nlu/test.py +++ b/rasa/nlu/test.py @@ -1762,7 +1762,7 @@ async def compare_nlu( configs: List[Text], data: TrainingData, exclusion_percentages: List[int], - f_score_results: Dict[Text, Any], + f_score_results: Dict[Text, List[List[float]]], model_names: List[Text], output: Text, runs: int, diff --git a/rasa/server.py b/rasa/server.py index 20af856c7801..1f1ebb0a921e 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -1236,7 +1236,7 @@ def _get_evaluation_results( "response_selection_evaluation": response_selector_report, } - result = defaultdict(dict) + result: Dict[Text, Any] = defaultdict(dict) for evaluation_name, evaluation in eval_name_mapping.items(): report = evaluation.evaluation.get("report", {}) averages = report.get("weighted avg", {}) From f70f50fe7810fffedafac077c35b02de75e9ef34 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 16:32:14 +0100 Subject: [PATCH 56/65] add type annotations in rasa.shared --- rasa/shared/constants.py | 5 ++++- rasa/shared/core/domain.py | 8 ++++---- rasa/shared/core/events.py | 2 +- rasa/shared/core/generator.py | 6 +++--- rasa/shared/core/trackers.py | 2 +- .../training_data/story_reader/story_reader.py | 2 +- .../story_reader/story_step_builder.py | 6 +++--- rasa/shared/core/training_data/structures.py | 3 ++- rasa/shared/core/training_data/visualization.py | 15 +++++++++++++-- rasa/shared/importers/multi_project.py | 4 ++-- .../shared/nlu/training_data/formats/rasa_yaml.py | 4 ++-- .../nlu/training_data/formats/readerwriter.py | 6 ++++-- rasa/shared/utils/io.py | 2 +- 13 files changed, 41 insertions(+), 24 deletions(-) diff --git a/rasa/shared/constants.py b/rasa/shared/constants.py index 0b00c1f1ef88..e77a4b8c95e8 100644 --- a/rasa/shared/constants.py +++ b/rasa/shared/constants.py @@ -1,3 +1,6 @@ +from typing import List, Text + + DOCS_BASE_URL = "https://rasa.com/docs/rasa" LEGACY_DOCS_BASE_URL = "https://legacy-docs-v1.rasa.com" DOCS_URL_TRAINING_DATA = DOCS_BASE_URL + "/training-data-format" @@ -68,7 +71,7 @@ CONFIG_KEYS_CORE = ["policies"] CONFIG_KEYS_NLU = ["language", "pipeline"] CONFIG_KEYS = CONFIG_KEYS_CORE + CONFIG_KEYS_NLU -CONFIG_MANDATORY_KEYS_CORE = [] +CONFIG_MANDATORY_KEYS_CORE: List[Text] = [] CONFIG_MANDATORY_KEYS_NLU = ["language"] CONFIG_MANDATORY_KEYS = CONFIG_MANDATORY_KEYS_CORE + CONFIG_MANDATORY_KEYS_NLU diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index c22785b31774..272fbd48d3ed 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -261,7 +261,7 @@ def _get_session_config(session_config: Dict) -> SessionConfig: @classmethod def from_directory(cls, path: Text) -> "Domain": """Loads and merges multiple domain files recursively from a directory tree.""" - domain_dict = {} + domain_dict: Dict[Text, Any] = {} for root, _, files in os.walk(path, followlinks=True): for file in files: full_path = os.path.join(root, file) @@ -350,7 +350,7 @@ def merge_domain_dicts( ) if merge_func == rasa.shared.utils.common.merge_dicts: - default = {} + default: Dict[Text, Any] = {} else: default = [] @@ -631,7 +631,7 @@ def collect_intent_properties( """ # make a copy to not alter the input argument intents = copy.deepcopy(intents) - intent_properties = {} + intent_properties: Dict[Text, Any] = {} duplicates = set() for intent in intents: @@ -1318,7 +1318,7 @@ def states_for_tracker_history( Return: A list of states. """ - states = [] + states: List[State] = [] last_ml_action_sub_state = None turn_was_hidden = False for tr, hide_rule_turn in tracker.generate_all_prior_trackers(): diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 92fbf266b0bb..113ba1d33ae7 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -182,7 +182,7 @@ def split_events( The split events. """ sub_events = [] - current = [] + current: List["Event"] = [] def event_fulfills_splitting_condition(evt: "Event") -> bool: # event does not have the correct type diff --git a/rasa/shared/core/generator.py b/rasa/shared/core/generator.py index fa681162f363..4f80f2e603bb 100644 --- a/rasa/shared/core/generator.py +++ b/rasa/shared/core/generator.py @@ -263,7 +263,7 @@ def __init__( rand=random.Random(42), ) # hashed featurization of all finished trackers - self.hashed_featurizations = set() + self.hashed_featurizations: Set[int] = set() @staticmethod def _phase_name(everything_reachable_is_reached: bool, phase: int) -> Text: @@ -344,8 +344,8 @@ def _generate( min_num_aug_phases = 0 # placeholder to track gluing process of checkpoints - used_checkpoints = set() - previous_unused = set() + used_checkpoints: Set[Text] = set() + previous_unused: Set[Text] = set() everything_reachable_is_reached = False # we will continue generating data until we have reached all diff --git a/rasa/shared/core/trackers.py b/rasa/shared/core/trackers.py index 5f4448371a7c..13d93c38d7e6 100644 --- a/rasa/shared/core/trackers.py +++ b/rasa/shared/core/trackers.py @@ -482,7 +482,7 @@ def applied_events(self) -> List[Event]: if isinstance(event, ActiveLoop) and event.name ] - applied_events = [] + applied_events: List[Event] = [] for event in self.events: if isinstance(event, (Restarted, SessionStarted)): diff --git a/rasa/shared/core/training_data/story_reader/story_reader.py b/rasa/shared/core/training_data/story_reader/story_reader.py index 6d2a32785f49..e3b4d5896f22 100644 --- a/rasa/shared/core/training_data/story_reader/story_reader.py +++ b/rasa/shared/core/training_data/story_reader/story_reader.py @@ -25,7 +25,7 @@ def __init__( domain: Domain object. source_name: Name of the training data source. """ - self.story_steps = [] + self.story_steps: List[StoryStep] = [] self.current_step_builder: Optional[StoryStepBuilder] = None self.domain = domain self.source_name = source_name diff --git a/rasa/shared/core/training_data/story_reader/story_step_builder.py b/rasa/shared/core/training_data/story_reader/story_step_builder.py index a156d2b335cf..f29e1cc94e48 100644 --- a/rasa/shared/core/training_data/story_reader/story_step_builder.py +++ b/rasa/shared/core/training_data/story_reader/story_step_builder.py @@ -23,9 +23,9 @@ def __init__( ) -> None: self.name = name self.source_name = source_name - self.story_steps = [] - self.current_steps = [] - self.start_checkpoints = [] + self.story_steps: List[StoryStep] = [] + self.current_steps: List[StoryStep] = [] + self.start_checkpoints: List[Checkpoint] = [] self.is_rule = is_rule def add_checkpoint(self, name: Text, conditions: Optional[Dict[Text, Any]]) -> None: diff --git a/rasa/shared/core/training_data/structures.py b/rasa/shared/core/training_data/structures.py index 685b6f024d52..3810c4e7491f 100644 --- a/rasa/shared/core/training_data/structures.py +++ b/rasa/shared/core/training_data/structures.py @@ -7,6 +7,7 @@ from typing import ( List, Text, + Deque, Dict, Optional, Tuple, @@ -716,7 +717,7 @@ def topological_sort( # noinspection PyPep8Naming GRAY, BLACK = 0, 1 - ordered = deque() + ordered: Deque = deque() unprocessed = sorted(set(graph)) visited_nodes = {} diff --git a/rasa/shared/core/training_data/visualization.py b/rasa/shared/core/training_data/visualization.py index e22dc03e3ed4..68662ff51a3b 100644 --- a/rasa/shared/core/training_data/visualization.py +++ b/rasa/shared/core/training_data/visualization.py @@ -1,7 +1,18 @@ from collections import defaultdict, deque import random -from typing import Any, Text, List, Dict, Optional, Set, TYPE_CHECKING, Union, cast +from typing import ( + Any, + Text, + List, + Deque, + Dict, + Optional, + Set, + TYPE_CHECKING, + Union, + cast, +) import rasa.shared.utils.io from rasa.shared.constants import INTENT_MESSAGE_PREFIX @@ -88,7 +99,7 @@ def _fingerprint_node( # the candidate list contains all node paths that haven't been # extended till `max_history` length yet. - candidates = deque() + candidates: Deque = deque() candidates.append([node]) continuations = [] while len(candidates) > 0: diff --git a/rasa/shared/importers/multi_project.py b/rasa/shared/importers/multi_project.py index 18ccdaa948ee..7b5f443dfb86 100644 --- a/rasa/shared/importers/multi_project.py +++ b/rasa/shared/importers/multi_project.py @@ -32,9 +32,9 @@ def __init__( else: self._domain_paths = [] self._story_paths = [] - self._e2e_story_paths = [] + self._e2e_story_paths: List[Text] = [] self._nlu_paths = [] - self._imports = [] + self._imports: List[Text] = [] self._additional_paths = training_data_paths or [] self._project_directory = project_directory or os.path.dirname(config_file) diff --git a/rasa/shared/nlu/training_data/formats/rasa_yaml.py b/rasa/shared/nlu/training_data/formats/rasa_yaml.py index adaf46d43c2f..9ec02d1235f2 100644 --- a/rasa/shared/nlu/training_data/formats/rasa_yaml.py +++ b/rasa/shared/nlu/training_data/formats/rasa_yaml.py @@ -429,7 +429,7 @@ def process_intents(cls, training_data: "TrainingData") -> List[OrderedDict]: @classmethod def process_synonyms(cls, training_data: "TrainingData") -> List[OrderedDict]: - inverted_synonyms = OrderedDict() + inverted_synonyms: Dict[Text, List[Dict]] = OrderedDict() for example, synonym in training_data.entity_synonyms.items(): if not inverted_synonyms.get(synonym): inverted_synonyms[synonym] = [] @@ -444,7 +444,7 @@ def process_synonyms(cls, training_data: "TrainingData") -> List[OrderedDict]: @classmethod def process_regexes(cls, training_data: "TrainingData") -> List[OrderedDict]: - inverted_regexes = OrderedDict() + inverted_regexes: Dict[Text, List[Text]] = OrderedDict() for regex in training_data.regex_features: if not inverted_regexes.get(regex["name"]): inverted_regexes[regex["name"]] = [] diff --git a/rasa/shared/nlu/training_data/formats/readerwriter.py b/rasa/shared/nlu/training_data/formats/readerwriter.py index b9fde20830ce..df0307949e39 100644 --- a/rasa/shared/nlu/training_data/formats/readerwriter.py +++ b/rasa/shared/nlu/training_data/formats/readerwriter.py @@ -77,12 +77,14 @@ def dumps(self, training_data: "TrainingData") -> Text: raise NotImplementedError @staticmethod - def prepare_training_examples(training_data: "TrainingData") -> OrderedDict: + def prepare_training_examples( + training_data: "TrainingData", + ) -> OrderedDict[Text, List[Union[Dict, Text]]]: """Pre-processes training data examples by removing not trainable entities.""" import rasa.shared.nlu.training_data.util as rasa_nlu_training_data_utils - training_examples = OrderedDict() + training_examples: OrderedDict[Text, List[Union[Dict, Text]]] = OrderedDict() # Sort by intent while keeping basic intent order for example in [e.as_dict_nlu() for e in training_data.training_examples]: diff --git a/rasa/shared/utils/io.py b/rasa/shared/utils/io.py index fd58fd774324..122fd256f31a 100644 --- a/rasa/shared/utils/io.py +++ b/rasa/shared/utils/io.py @@ -164,7 +164,7 @@ def list_directory(path: Text) -> List[Text]: if os.path.isfile(path): return [path] elif os.path.isdir(path): - results = [] + results: List[Text] = [] for base, dirs, files in os.walk(path, followlinks=True): # sort files for same order across runs files = sorted(files, key=_filename_without_prefix) From 5d70ce822ce98945523289daf67d9e7fad3a5763 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 17:08:46 +0100 Subject: [PATCH 57/65] fix OrderedDict type --- .../lexical_syntactic_featurizer.py | 19 +++++++++++++++---- .../nlu/training_data/formats/readerwriter.py | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py b/rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py index 205946cb37f8..97e11b54089b 100644 --- a/rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py +++ b/rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py @@ -4,7 +4,18 @@ import scipy.sparse import numpy as np -from typing import Any, Dict, Text, List, Tuple, Callable, Set, Optional, Type, Union +from typing import ( + Any, + Dict, + Text, + List, + Tuple, + Callable, + Set, + Optional, + Type, + Union, +) from rasa.engine.graph import ExecutionContext, GraphComponent from rasa.engine.recipes.default_recipe import DefaultV1Recipe @@ -359,9 +370,9 @@ def _build_feature_to_index_map( """ # Note that this will only sort the top level keys - and we keep # doing it to ensure consistently with what was done before) - ordered_feature_vocabulary: OrderedDict[ - Tuple[int, Text], Set[Text] - ] = OrderedDict(sorted(feature_vocabulary.items())) + ordered_feature_vocabulary: Dict[Tuple[int, Text], Set[Text]] = OrderedDict( + sorted(feature_vocabulary.items()) + ) # create the nested mapping feature_to_idx_dict: Dict[Tuple[int, Text], Dict[Text, int]] = {} diff --git a/rasa/shared/nlu/training_data/formats/readerwriter.py b/rasa/shared/nlu/training_data/formats/readerwriter.py index df0307949e39..d500a8e740f0 100644 --- a/rasa/shared/nlu/training_data/formats/readerwriter.py +++ b/rasa/shared/nlu/training_data/formats/readerwriter.py @@ -79,12 +79,12 @@ def dumps(self, training_data: "TrainingData") -> Text: @staticmethod def prepare_training_examples( training_data: "TrainingData", - ) -> OrderedDict[Text, List[Union[Dict, Text]]]: + ) -> Dict[Text, List[Union[Dict, Text]]]: """Pre-processes training data examples by removing not trainable entities.""" import rasa.shared.nlu.training_data.util as rasa_nlu_training_data_utils - training_examples: OrderedDict[Text, List[Union[Dict, Text]]] = OrderedDict() + training_examples: Dict[Text, List[Union[Dict, Text]]] = OrderedDict() # Sort by intent while keeping basic intent order for example in [e.as_dict_nlu() for e in training_data.training_examples]: From b8433f14992151dd816028d9473aa58862539bb3 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Tue, 1 Mar 2022 17:16:18 +0100 Subject: [PATCH 58/65] fix docstring issues --- rasa/core/channels/channel.py | 2 ++ rasa/core/lock_store.py | 4 ++++ rasa/core/training/interactive.py | 2 -- .../core/training_data/story_reader/story_step_builder.py | 2 +- rasa/shared/nlu/training_data/formats/rasa_yaml.py | 2 ++ rasa/shared/nlu/training_data/formats/readerwriter.py | 1 - 6 files changed, 9 insertions(+), 4 deletions(-) diff --git a/rasa/core/channels/channel.py b/rasa/core/channels/channel.py index 886dc45dc484..2efd680b183f 100644 --- a/rasa/core/channels/channel.py +++ b/rasa/core/channels/channel.py @@ -330,10 +330,12 @@ class CollectingOutputChannel(OutputChannel): (doesn't send them anywhere, just collects them).""" def __init__(self) -> None: + """Initialise list to collect messages.""" self.messages: List[Dict[Text, Any]] = [] @classmethod def name(cls) -> Text: + """Name of the channel.""" return "collector" @staticmethod diff --git a/rasa/core/lock_store.py b/rasa/core/lock_store.py index 39d685ec711c..e691b2ff2c48 100644 --- a/rasa/core/lock_store.py +++ b/rasa/core/lock_store.py @@ -274,19 +274,23 @@ class InMemoryLockStore(LockStore): """In-memory store for ticket locks.""" def __init__(self) -> None: + """Initialise dictionary of locks.""" self.conversation_locks: Dict[Text, TicketLock] = {} super().__init__() def get_lock(self, conversation_id: Text) -> Optional[TicketLock]: + """Get lock for conversation if it exists.""" return self.conversation_locks.get(conversation_id) def delete_lock(self, conversation_id: Text) -> None: + """Delete lock for conversation.""" deleted_lock = self.conversation_locks.pop(conversation_id, None) self._log_deletion( conversation_id, deletion_successful=deleted_lock is not None ) def save_lock(self, lock: TicketLock) -> None: + """Save lock in store.""" self.conversation_locks[lock.conversation_id] = lock diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py index 504272c0cd22..1f276d01262b 100644 --- a/rasa/core/training/interactive.py +++ b/rasa/core/training/interactive.py @@ -318,7 +318,6 @@ async def _ask_questions( is_abort: Callable[[Dict[Text, Any]], bool] = lambda x: False, ) -> Any: """Ask the user a question, if Ctrl-C is pressed provide user with menu.""" - should_retry = True answers: Any = {} @@ -335,7 +334,6 @@ def _selection_choices_from_intent_prediction( predictions: List[Dict[Text, Any]] ) -> List[Dict[Text, Any]]: """Given a list of ML predictions create a UI choice list.""" - sorted_intents = sorted( predictions, key=lambda k: (-k["confidence"], k[INTENT_NAME_KEY]) ) diff --git a/rasa/shared/core/training_data/story_reader/story_step_builder.py b/rasa/shared/core/training_data/story_reader/story_step_builder.py index f29e1cc94e48..91b80d37b10a 100644 --- a/rasa/shared/core/training_data/story_reader/story_step_builder.py +++ b/rasa/shared/core/training_data/story_reader/story_step_builder.py @@ -29,7 +29,7 @@ def __init__( self.is_rule = is_rule def add_checkpoint(self, name: Text, conditions: Optional[Dict[Text, Any]]) -> None: - + """Add a checkpoint to story steps.""" # Depending on the state of the story part this # is either a start or an end check point if not self.current_steps: diff --git a/rasa/shared/nlu/training_data/formats/rasa_yaml.py b/rasa/shared/nlu/training_data/formats/rasa_yaml.py index 9ec02d1235f2..9bfc060c5ced 100644 --- a/rasa/shared/nlu/training_data/formats/rasa_yaml.py +++ b/rasa/shared/nlu/training_data/formats/rasa_yaml.py @@ -429,6 +429,7 @@ def process_intents(cls, training_data: "TrainingData") -> List[OrderedDict]: @classmethod def process_synonyms(cls, training_data: "TrainingData") -> List[OrderedDict]: + """Serializes the synonyms.""" inverted_synonyms: Dict[Text, List[Dict]] = OrderedDict() for example, synonym in training_data.entity_synonyms.items(): if not inverted_synonyms.get(synonym): @@ -444,6 +445,7 @@ def process_synonyms(cls, training_data: "TrainingData") -> List[OrderedDict]: @classmethod def process_regexes(cls, training_data: "TrainingData") -> List[OrderedDict]: + """Serializes the regexes.""" inverted_regexes: Dict[Text, List[Text]] = OrderedDict() for regex in training_data.regex_features: if not inverted_regexes.get(regex["name"]): diff --git a/rasa/shared/nlu/training_data/formats/readerwriter.py b/rasa/shared/nlu/training_data/formats/readerwriter.py index d500a8e740f0..50475f2b1f86 100644 --- a/rasa/shared/nlu/training_data/formats/readerwriter.py +++ b/rasa/shared/nlu/training_data/formats/readerwriter.py @@ -81,7 +81,6 @@ def prepare_training_examples( training_data: "TrainingData", ) -> Dict[Text, List[Union[Dict, Text]]]: """Pre-processes training data examples by removing not trainable entities.""" - import rasa.shared.nlu.training_data.util as rasa_nlu_training_data_utils training_examples: Dict[Text, List[Union[Dict, Text]]] = OrderedDict() From 702d3da1108569f6a81b38dc45f82069d7ace014 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Wed, 2 Mar 2022 16:06:17 +0100 Subject: [PATCH 59/65] use correct defaultdict type --- rasa/core/policies/rule_policy.py | 4 +++- rasa/core/training/story_conflict.py | 4 ++-- rasa/core/training/training.py | 2 +- rasa/server.py | 2 +- rasa/utils/tensorflow/model_data.py | 14 +++++++------- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/rasa/core/policies/rule_policy.py b/rasa/core/policies/rule_policy.py index 702f13ecb1ea..b587eb1d056c 100644 --- a/rasa/core/policies/rule_policy.py +++ b/rasa/core/policies/rule_policy.py @@ -159,7 +159,9 @@ def __init__( self._enable_fallback_prediction = config["enable_fallback_prediction"] self._check_for_contradictions = config["check_for_contradictions"] - self._rules_sources: Dict[Text, List[Tuple[Text, Text]]] = defaultdict(list) + self._rules_sources: defaultdict[Text, List[Tuple[Text, Text]]] = defaultdict( + list + ) @classmethod def raise_if_incompatible_with_domain( diff --git a/rasa/core/training/story_conflict.py b/rasa/core/training/story_conflict.py index 281058ebbe96..0bde5528cf05 100644 --- a/rasa/core/training/story_conflict.py +++ b/rasa/core/training/story_conflict.py @@ -38,7 +38,7 @@ def __init__(self, sliced_states: List[State]) -> None: self._sliced_states = sliced_states # A list of actions that all follow from the same state. - self._conflicting_actions: Dict[Text, List[Text]] = defaultdict( + self._conflicting_actions: defaultdict[Text, List[Text]] = defaultdict( list ) # {"action": ["story_1", ...], ...} @@ -196,7 +196,7 @@ def _find_conflicting_states( """ # Create a 'state -> list of actions' dict, where the state is # represented by its hash - state_action_mapping: Dict[int, List[int]] = defaultdict(list) + state_action_mapping: defaultdict[int, List[int]] = defaultdict(list) for element in _sliced_states_iterator(trackers, domain, max_history, tokenizer): hashed_state = element.sliced_states_hash diff --git a/rasa/core/training/training.py b/rasa/core/training/training.py index 16037e6fb2e7..9c9af2768504 100644 --- a/rasa/core/training/training.py +++ b/rasa/core/training/training.py @@ -65,7 +65,7 @@ def create_action_fingerprints( # take into account only featurized slots featurized_slots = {slot.name for slot in domain.slots if slot.has_features()} - action_fingerprints: Dict[Text, Dict[Text, List[Text]]] = defaultdict(dict) + action_fingerprints: defaultdict[Text, Dict[Text, List[Text]]] = defaultdict(dict) for action_name, events_after_action in events_after_actions.items(): slots = list( set( diff --git a/rasa/server.py b/rasa/server.py index 1f1ebb0a921e..08efffbe55f4 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -1236,7 +1236,7 @@ def _get_evaluation_results( "response_selection_evaluation": response_selector_report, } - result: Dict[Text, Any] = defaultdict(dict) + result: defaultdict[Text, Any] = defaultdict(dict) for evaluation_name, evaluation in eval_name_mapping.items(): report = evaluation.evaluation.get("report", {}) averages = report.get("weighted avg", {}) diff --git a/rasa/utils/tensorflow/model_data.py b/rasa/utils/tensorflow/model_data.py index 99a71ae43319..c1740910a5dc 100644 --- a/rasa/utils/tensorflow/model_data.py +++ b/rasa/utils/tensorflow/model_data.py @@ -695,9 +695,9 @@ def balanced_data(self, data: Data, batch_size: int, shuffle: bool) -> Data: # if a label was skipped in current batch skipped = [False] * num_label_ids - new_data: Dict[Text, Dict[Text, List[List[FeatureArray]]]] = defaultdict( - lambda: defaultdict(list) - ) + new_data: defaultdict[ + Text, defaultdict[Text, List[List[FeatureArray]]] + ] = defaultdict(lambda: defaultdict(list)) while min(num_data_cycles) == 0: if shuffle: @@ -848,10 +848,10 @@ def _convert_train_test_split( Returns: The test and train RasaModelData """ - data_train: Dict[Text, Dict[Text, List[FeatureArray]]] = defaultdict( - lambda: defaultdict(list) - ) - data_val: Dict[Text, Dict[Text, List[Any]]] = defaultdict( + data_train: defaultdict[ + Text, defaultdict[Text, List[FeatureArray]] + ] = defaultdict(lambda: defaultdict(list)) + data_val: defaultdict[Text, defaultdict[Text, List[Any]]] = defaultdict( lambda: defaultdict(list) ) From d5414d7865187a026db6c39d327b9bb7c099de36 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Thu, 3 Mar 2022 09:55:48 +0100 Subject: [PATCH 60/65] add comment to explain type issue in RulePolicy --- rasa/core/policies/rule_policy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rasa/core/policies/rule_policy.py b/rasa/core/policies/rule_policy.py index b587eb1d056c..c6a06e4ca156 100644 --- a/rasa/core/policies/rule_policy.py +++ b/rasa/core/policies/rule_policy.py @@ -566,6 +566,9 @@ def _check_prediction( gold_action_name: Text, prediction_source: Optional[Text], ) -> List[Text]: + # FIXME: `predicted_action_name` and `prediction_source` are + # either None together or defined together. This could be improved + # by better typing in this class, but requires some refactoring if ( not predicted_action_name or not prediction_source From ec8de9bc19c20880bf681a686f776523b612cc27 Mon Sep 17 00:00:00 2001 From: Anca Lita <27920906+ancalita@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:15:23 +0000 Subject: [PATCH 61/65] Enable `union-attr` mypy check and fix issues (#10942) * first batch of mypy fixes * fix for failing tests * fix errors in channels and shared packages * add docstring, fix error in brokers and lock module * more fixes * fix more union-attr errors * add more fixes * fix errors in action module, undo a different change * more fixes in agent and diet classifier modules * fix more errors * fixes for failing tests * fix errors in migrate and policies modules * address review comments - first part * address review comments - part 2 * add fix in generator module * apply review suggestions - part 3 * fix error in domain module * fix final errors * address final review comments * add changelog entries * fix docstring --- changelog/9096.misc.md | 1 + changelog/9098.misc.md | 1 + rasa/core/actions/action.py | 40 ++++++++++++++----- rasa/core/actions/forms.py | 8 ++-- rasa/core/agent.py | 25 +++++++----- rasa/core/brokers/kafka.py | 8 +++- rasa/core/brokers/pika.py | 6 +++ rasa/core/channels/hangouts.py | 2 +- rasa/core/channels/rest.py | 9 ++++- rasa/core/channels/twilio_voice.py | 21 ++++++---- .../featurizers/single_state_featurizer.py | 8 ++-- rasa/core/featurizers/tracker_featurizers.py | 34 +++++++++------- rasa/core/lock.py | 15 +------ rasa/core/migrate.py | 40 +++++++++++++------ rasa/core/nlg/response.py | 2 +- rasa/core/policies/memoization.py | 5 ++- rasa/core/policies/rule_policy.py | 6 ++- rasa/core/policies/ted_policy.py | 22 ++++++++-- .../core/policies/unexpected_intent_policy.py | 13 +++++- rasa/core/processor.py | 7 ++-- rasa/core/test.py | 16 +++++++- rasa/core/tracker_store.py | 35 ++++++++-------- rasa/core/training/interactive.py | 9 ++++- rasa/engine/runner/dask.py | 3 ++ rasa/engine/storage/local_model_storage.py | 3 ++ rasa/engine/validation.py | 3 +- rasa/jupyter.py | 7 ++++ rasa/nlu/classifiers/diet_classifier.py | 11 ++++- .../classifiers/sklearn_intent_classifier.py | 10 ++++- rasa/nlu/extractors/mitie_entity_extractor.py | 9 +++-- rasa/shared/core/conversation.py | 3 +- rasa/shared/core/domain.py | 2 +- rasa/shared/core/events.py | 6 +++ rasa/shared/core/generator.py | 11 +++-- rasa/shared/core/slot_mappings.py | 10 ++++- rasa/shared/core/trackers.py | 2 + .../story_reader/story_reader.py | 2 + .../story_reader/yaml_story_reader.py | 7 ++-- rasa/shared/core/training_data/structures.py | 2 +- .../core/training_data/visualization.py | 16 +++++--- rasa/shared/nlu/training_data/loading.py | 12 +++--- rasa/telemetry.py | 7 +++- rasa/utils/tensorflow/model_data_utils.py | 2 + rasa/utils/tensorflow/rasa_layers.py | 7 ++-- rasa/utils/tensorflow/temp_keras_modules.py | 17 ++++---- rasa/validator.py | 7 ++++ setup.cfg | 3 +- 47 files changed, 333 insertions(+), 162 deletions(-) create mode 100644 changelog/9096.misc.md create mode 100644 changelog/9098.misc.md diff --git a/changelog/9096.misc.md b/changelog/9096.misc.md new file mode 100644 index 000000000000..96e2d2fe90f5 --- /dev/null +++ b/changelog/9096.misc.md @@ -0,0 +1 @@ +Enable `mypy` `union-attr` check and fix any resulting errors. diff --git a/changelog/9098.misc.md b/changelog/9098.misc.md new file mode 100644 index 000000000000..0ed1e4791401 --- /dev/null +++ b/changelog/9098.misc.md @@ -0,0 +1 @@ +Enable `mypy` `attr-defined` check and fix any resulting errors. diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index fcf621d10db2..4f5bb3b48e2e 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -1,7 +1,17 @@ import copy import json import logging -from typing import List, Text, Optional, Dict, Any, TYPE_CHECKING, Tuple, Set, Union +from typing import ( + List, + Text, + Optional, + Dict, + Any, + TYPE_CHECKING, + Tuple, + Set, + cast, +) import aiohttp import rasa.core @@ -387,10 +397,15 @@ def get_full_retrieval_name( Full retrieval name of the action if the last user utterance contains a response selector output, `None` otherwise. """ - if RESPONSE_SELECTOR_PROPERTY_NAME not in tracker.latest_message.parse_data: + latest_message = tracker.latest_message + + if latest_message is None: + return None + + if RESPONSE_SELECTOR_PROPERTY_NAME not in latest_message.parse_data: return None - response_selector_properties = tracker.latest_message.parse_data[ + response_selector_properties = latest_message.parse_data[ RESPONSE_SELECTOR_PROPERTY_NAME ] @@ -418,7 +433,12 @@ async def run( domain: "Domain", ) -> List[Event]: """Query the appropriate response and create a bot utterance with that.""" - response_selector_properties = tracker.latest_message.parse_data[ + latest_message = tracker.latest_message + + if latest_message is None: + return [] + + response_selector_properties = latest_message.parse_data[ RESPONSE_SELECTOR_PROPERTY_NAME ] @@ -719,7 +739,7 @@ async def run( logger.debug( "Calling action endpoint to run action '{}'.".format(self.name()) ) - response = await self.action_endpoint.request( + response: Any = await self.action_endpoint.request( json=json_body, method="post", timeout=DEFAULT_REQUEST_TIMEOUT ) @@ -1091,7 +1111,7 @@ async def _execute_validation_action( tracker: "DialogueStateTracker", domain: "Domain", ) -> List[Event]: - slot_events: List[Union[Event, SlotSet]] = [ + slot_events: List[SlotSet] = [ event for event in extraction_events if isinstance(event, SlotSet) ] @@ -1099,11 +1119,11 @@ async def _execute_validation_action( logger.debug(f"Validating extracted slots: {slot_candidates}") if ACTION_VALIDATE_SLOT_MAPPINGS not in domain.user_actions: - return slot_events + return cast(List[Event], slot_events) _tracker = DialogueStateTracker.from_events( tracker.sender_id, - tracker.events_after_latest_restart() + slot_events, + tracker.events_after_latest_restart() + cast(List[Event], slot_events), slots=domain.slots, ) validate_events = await self._run_custom_action( @@ -1267,6 +1287,8 @@ def extract_slot_value_from_predefined_mapping( elif should_fill_intent_slot or should_fill_trigger_slot: value = [mapping.get("value")] elif should_fill_text_slot: - value = [tracker.latest_message.text] + value = [ + tracker.latest_message.text if tracker.latest_message is not None else None + ] return value diff --git a/rasa/core/actions/forms.py b/rasa/core/actions/forms.py index 0c68086ccc94..973c9e499543 100644 --- a/rasa/core/actions/forms.py +++ b/rasa/core/actions/forms.py @@ -104,8 +104,8 @@ def get_mappings_for_slot( If None, map requested slot to an entity with the same name """ - domain_slots = domain.as_dict().get(KEY_SLOTS) - requested_slot_mappings = domain_slots.get(slot_to_fill).get("mappings") + domain_slots = domain.as_dict().get(KEY_SLOTS, {}) + requested_slot_mappings = domain_slots.get(slot_to_fill, {}).get("mappings", []) # check provided slot mappings for requested_slot_mapping in requested_slot_mappings: @@ -149,9 +149,9 @@ def _create_unique_entity_mappings(self, domain: Domain) -> Set[Text]: """ unique_entity_slot_mappings = set() duplicate_entity_slot_mappings = set() - domain_slots = domain.as_dict().get(KEY_SLOTS) + domain_slots = domain.as_dict().get(KEY_SLOTS, {}) for slot in domain.required_slots_for_form(self.name()): - for slot_mapping in domain_slots.get(slot).get(SLOT_MAPPINGS): + for slot_mapping in domain_slots.get(slot, {}).get(SLOT_MAPPINGS, []): if slot_mapping.get(MAPPING_TYPE) == str(SlotMappingType.FROM_ENTITY): mapping_as_string = json.dumps(slot_mapping, sort_keys=True) if mapping_as_string in unique_entity_slot_mappings: diff --git a/rasa/core/agent.py b/rasa/core/agent.py index 9813ec3a1eed..b0f1ff018205 100644 --- a/rasa/core/agent.py +++ b/rasa/core/agent.py @@ -292,7 +292,7 @@ class Agent: def __init__( self, - domain: Optional[Union[Text, Domain]] = None, + domain: Optional[Domain] = None, generator: Union[EndpointConfig, NaturalLanguageGenerator, None] = None, tracker_store: Optional[TrackerStore] = None, lock_store: Optional[LockStore] = None, @@ -320,7 +320,7 @@ def __init__( def load( cls, model_path: Union[Text, Path], - domain: Optional[Union[Text, Domain]] = None, + domain: Optional[Domain] = None, generator: Union[EndpointConfig, NaturalLanguageGenerator, None] = None, tracker_store: Optional[TrackerStore] = None, lock_store: Optional[LockStore] = None, @@ -405,7 +405,8 @@ async def parse_message(self, message_data: Text) -> Dict[Text, Any]: """ message = UserMessage(message_data) - return await self.processor.parse_message(message) + + return await self.processor.parse_message(message) # type: ignore[union-attr] async def handle_message( self, message: UserMessage @@ -416,14 +417,18 @@ async def handle_message( return None async with self.lock_store.lock(message.sender_id): - return await self.processor.handle_message(message) + return await self.processor.handle_message( # type: ignore[union-attr] + message + ) @agent_must_be_ready async def predict_next_for_sender_id( self, sender_id: Text ) -> Optional[Dict[Text, Any]]: """Predict the next action for a sender id.""" - return await self.processor.predict_next_for_sender_id(sender_id) + return await self.processor.predict_next_for_sender_id( # type: ignore[union-attr] # noqa:E501 + sender_id + ) @agent_must_be_ready def predict_next_with_tracker( @@ -432,12 +437,14 @@ def predict_next_with_tracker( verbosity: EventVerbosity = EventVerbosity.AFTER_RESTART, ) -> Optional[Dict[Text, Any]]: """Predicts the next action.""" - return self.processor.predict_next_with_tracker(tracker, verbosity) + return self.processor.predict_next_with_tracker( # type: ignore[union-attr] + tracker, verbosity + ) @agent_must_be_ready async def log_message(self, message: UserMessage) -> DialogueStateTracker: """Append a message to a dialogue - does not predict actions.""" - return await self.processor.log_message(message) + return await self.processor.log_message(message) # type: ignore[union-attr] @agent_must_be_ready async def execute_action( @@ -452,7 +459,7 @@ async def execute_action( prediction = PolicyPrediction.for_action_name( self.domain, action, policy, confidence or 0.0 ) - return await self.processor.execute_action( + return await self.processor.execute_action( # type: ignore[union-attr] sender_id, action, output_channel, self.nlg, prediction ) @@ -465,7 +472,7 @@ async def trigger_intent( tracker: DialogueStateTracker, ) -> None: """Trigger a user intent, e.g. triggered by an external event.""" - await self.processor.trigger_external_user_uttered( + await self.processor.trigger_external_user_uttered( # type: ignore[union-attr] intent_name, entities, tracker, output_channel ) diff --git a/rasa/core/brokers/kafka.py b/rasa/core/brokers/kafka.py index 53512270670c..adb829251e03 100644 --- a/rasa/core/brokers/kafka.py +++ b/rasa/core/brokers/kafka.py @@ -209,10 +209,14 @@ def _publish(self, event: Dict[Text, Any]) -> None: f" key={partition_key!s}, headers={headers})" ) - self.producer.send(self.topic, value=event, key=partition_key, headers=headers) + if self.producer is not None: + self.producer.send( + self.topic, value=event, key=partition_key, headers=headers + ) def _close(self) -> None: - self.producer.close() + if self.producer is not None: + self.producer.close() @rasa.shared.utils.common.lazy_property def rasa_environment(self) -> Optional[Text]: diff --git a/rasa/core/brokers/pika.py b/rasa/core/brokers/pika.py index b094f6affff2..205a3bbca030 100644 --- a/rasa/core/brokers/pika.py +++ b/rasa/core/brokers/pika.py @@ -244,6 +244,9 @@ async def close(self) -> None: def is_ready(self) -> bool: """Return `True` if a connection was established.""" + if self._connection is None: + return False + return not self._connection.is_closed def publish( @@ -262,6 +265,9 @@ def publish( async def _publish( self, event: Dict[Text, Any], headers: Optional[Dict[Text, Text]] = None ) -> None: + if self._exchange is None: + return + try: await self._exchange.publish(self._message(event, headers), "") diff --git a/rasa/core/channels/hangouts.py b/rasa/core/channels/hangouts.py index 1a7c19a96867..8f840854973b 100644 --- a/rasa/core/channels/hangouts.py +++ b/rasa/core/channels/hangouts.py @@ -297,7 +297,7 @@ async def health(request: Request) -> HTTPResponse: async def receive(request: Request) -> HTTPResponse: if self.project_id: - token = request.headers.get("Authorization").replace("Bearer ", "") + token = request.headers.get("Authorization", "").replace("Bearer ", "") self._check_token(token) sender_id = self._extract_sender(request) diff --git a/rasa/core/channels/rest.py b/rasa/core/channels/rest.py index 0cd8c04c0f7b..3d0541330e90 100644 --- a/rasa/core/channels/rest.py +++ b/rasa/core/channels/rest.py @@ -86,9 +86,16 @@ async def stream(resp: Any) -> None: def blueprint( self, on_new_message: Callable[[UserMessage], Awaitable[None]] ) -> Blueprint: + """Groups the collection of endpoints used by rest channel.""" + module_type = inspect.getmodule(self) + if module_type is not None: + module_name = module_type.__name__ + else: + module_name = None + custom_webhook = Blueprint( "custom_webhook_{}".format(type(self).__name__), - inspect.getmodule(self).__name__, + module_name, ) # noinspection PyUnusedLocal diff --git a/rasa/core/channels/twilio_voice.py b/rasa/core/channels/twilio_voice.py index 5131910f55c9..85d1c9dbb7b3 100644 --- a/rasa/core/channels/twilio_voice.py +++ b/rasa/core/channels/twilio_voice.py @@ -117,9 +117,9 @@ def __init__( initial_prompt: Optional[Text], reprompt_fallback_phrase: Optional[Text], assistant_voice: Optional[Text], - speech_timeout: Optional[Text], - speech_model: Optional[Text], - enhanced: Optional[Text], + speech_timeout: Text = "5", + speech_model: Text = "default", + enhanced: Text = "false", ) -> None: """Creates a connection to Twilio voice. @@ -154,16 +154,21 @@ def _validate_configuration(self) -> None: if self.speech_model not in self.SUPPORTED_SPEECH_MODELS: self._raise_invalid_speech_model_exception() - if self.enhanced.lower() not in ["true", "false"]: + if self.enhanced.lower() not in [ + "true", + "false", + ]: self._raise_invalid_enhanced_option_exception() - if (self.enhanced.lower() == "true") and ( - self.speech_model.lower() != "phone_call" + if ( + self.enhanced.lower() == "true" + and self.speech_model.lower() != "phone_call" ): self._raise_invalid_enhanced_speech_model_exception() - if (self.speech_model.lower() != "numbers_and_commands") and ( - self.speech_timeout.lower() == "auto" + if ( + self.speech_model.lower() != "numbers_and_commands" + and self.speech_timeout.lower() == "auto" ): self._raise_invalid_speech_model_timeout_exception() diff --git a/rasa/core/featurizers/single_state_featurizer.py b/rasa/core/featurizers/single_state_featurizer.py index c9e657aed473..2705463cc434 100644 --- a/rasa/core/featurizers/single_state_featurizer.py +++ b/rasa/core/featurizers/single_state_featurizer.py @@ -309,9 +309,11 @@ def encode_entities( ): # we cannot build a classifier with fewer than 2 classes return {} - - message = precomputations.lookup_message(user_text=entity_data[TEXT]) - message.data[ENTITIES] = entity_data[ENTITIES] + if precomputations is None: + message = None + else: + message = precomputations.lookup_message(user_text=entity_data[TEXT]) + message.data[ENTITIES] = entity_data[ENTITIES] if not message: return {} diff --git a/rasa/core/featurizers/tracker_featurizers.py b/rasa/core/featurizers/tracker_featurizers.py index 1abb310f4de3..04f0b5a6b1f1 100644 --- a/rasa/core/featurizers/tracker_featurizers.py +++ b/rasa/core/featurizers/tracker_featurizers.py @@ -105,13 +105,16 @@ def _featurize_states( Returns: Featurized tracker states. """ - return [ - [ - self.state_featurizer.encode_state(state, precomputations) - for state in tracker_states + if self.state_featurizer is None: + return [[{}]] + else: + return [ + [ + self.state_featurizer.encode_state(state, precomputations) + for state in tracker_states + ] + for tracker_states in trackers_as_states ] - for tracker_states in trackers_as_states - ] @staticmethod def _convert_labels_to_ids( @@ -152,15 +155,18 @@ def _create_entity_tags( Returns: Trackers as entity features. """ - return [ - [ - self.state_featurizer.encode_entities( - entity_data, precomputations, bilou_tagging - ) - for entity_data in trackers_entities + if self.state_featurizer is None: + return [[{}]] + else: + return [ + [ + self.state_featurizer.encode_entities( + entity_data, precomputations, bilou_tagging + ) + for entity_data in trackers_entities + ] + for trackers_entities in trackers_as_entities ] - for trackers_entities in trackers_as_entities - ] @staticmethod def _entity_data(event: UserUttered) -> Dict[Text, Any]: diff --git a/rasa/core/lock.py b/rasa/core/lock.py index d148440001df..7195ecbb1794 100644 --- a/rasa/core/lock.py +++ b/rasa/core/lock.py @@ -23,13 +23,11 @@ def as_dict(self) -> Dict[Text, Any]: def dumps(self) -> Text: """Return json dump of `Ticket` as dictionary.""" - return json.dumps(self.as_dict()) @classmethod def from_dict(cls, data: Dict[Text, Union[int, float]]) -> "Ticket": """Creates `Ticket` from dictionary.""" - return cls(number=data["number"], expires=data["expires"]) def __repr__(self) -> Text: @@ -53,13 +51,11 @@ def __init__( @classmethod def from_dict(cls, data: Dict[Text, Any]) -> "TicketLock": """Create `TicketLock` from dictionary.""" - - tickets = [Ticket.from_dict(json.loads(d)) for d in data.get("tickets")] + tickets = [Ticket.from_dict(json.loads(d)) for d in data.get("tickets", [])] return cls(data.get("conversation_id"), deque(tickets)) def dumps(self) -> Text: """Return json dump of `TicketLock`.""" - tickets = [ticket.dumps() for ticket in self.tickets] return json.dumps(dict(conversation_id=self.conversation_id, tickets=tickets)) @@ -69,12 +65,10 @@ def is_locked(self, ticket_number: int) -> bool: Returns: True if `now_serving` is not equal to `ticket`. """ - return self.now_serving != ticket_number def issue_ticket(self, lifetime: float) -> int: """Issue a new ticket and return its number.""" - self.remove_expired_tickets() number = self.last_issued + 1 ticket = Ticket(number, time.time() + lifetime) @@ -84,7 +78,6 @@ def issue_ticket(self, lifetime: float) -> int: def remove_expired_tickets(self) -> None: """Remove expired tickets.""" - # iterate over copy of self.tickets so we can remove items for ticket in list(self.tickets): if ticket.has_expired(): @@ -98,7 +91,6 @@ def last_issued(self) -> int: Number of `Ticket` that was last added. `NO_TICKET_ISSUED` if no tickets exist. """ - ticket_number = self._ticket_number_for(-1) return ticket_number if ticket_number is not None else NO_TICKET_ISSUED @@ -110,7 +102,6 @@ def now_serving(self) -> Optional[int]: Returns: Number of `Ticket` that is served next. 0 if no `Ticket` exists. """ - return self._ticket_number_for(0) or 0 def _ticket_number_for(self, ticket_index: int) -> Optional[int]: @@ -120,7 +111,6 @@ def _ticket_number_for(self, ticket_index: int) -> Optional[int]: Ticket number for `Ticket` with index `ticket_index`. None if there are no tickets, or if `ticket_index` is out of bounds of `self.tickets`. """ - self.remove_expired_tickets() try: @@ -130,7 +120,6 @@ def _ticket_number_for(self, ticket_index: int) -> Optional[int]: def _ticket_for_ticket_number(self, ticket_number: int) -> Optional[Ticket]: """Return ticket for `ticket_number`.""" - self.remove_expired_tickets() return next((t for t in self.tickets if t.number == ticket_number), None) @@ -141,12 +130,10 @@ def is_someone_waiting(self) -> bool: Returns: True if the `self.tickets` queue has length greater than 0. """ - return len(self.tickets) > 0 def remove_ticket_for(self, ticket_number: int) -> None: """Remove `Ticket` for `ticket_number.""" - ticket = self._ticket_for_ticket_number(ticket_number) if ticket: self.tickets.remove(ticket) diff --git a/rasa/core/migrate.py b/rasa/core/migrate.py index 8de15d0fb63b..98530a1d25f9 100644 --- a/rasa/core/migrate.py +++ b/rasa/core/migrate.py @@ -18,17 +18,18 @@ from rasa.shared.constants import LATEST_TRAINING_DATA_FORMAT_VERSION from rasa.shared.core.domain import KEY_ENTITIES, KEY_SLOTS, KEY_FORMS, Domain from rasa.shared.exceptions import RasaException +from rasa.shared.utils.validation import KEY_TRAINING_DATA_FORMAT_VERSION ORIGINAL_DOMAIN = "original_domain" # not a default, fixed DEFAULT_NEW_DOMAIN = "new_domain" YML_SUFFIX = ".yml" -def _create_back_up( - domain_file: Path, backup_location: Path -) -> Union[List[Any], Dict[Text, Any]]: +def _create_back_up(domain_file: Path, backup_location: Path) -> Dict[Text, Any]: """Makes a backup and returns the content of the file.""" - original_content = rasa.shared.utils.io.read_yaml_file(domain_file) + original_content = rasa.shared.utils.io.read_yaml( + rasa.shared.utils.io.read_file(domain_file) + ) rasa.shared.utils.io.write_yaml( original_content, backup_location, should_preserve_key_order=True ) @@ -167,7 +168,9 @@ def _migrate_auto_fill_and_custom_slots( def _assemble_new_domain( domain_file: Path, new_forms: Dict[Text, Any], new_slots: Dict[Text, Any] ) -> Dict[Text, Any]: - original_content = rasa.shared.utils.io.read_yaml_file(domain_file) + original_content = rasa.shared.utils.io.read_yaml( + rasa.shared.utils.io.read_file(domain_file) + ) new_domain: Dict[Text, Any] = {} for key, value in original_content.items(): if key == KEY_SLOTS: @@ -326,15 +329,28 @@ def migrate_domain_format( # Note: we do not enforce that the version tag is 2.0 everywhere + validate that # migrate-able domain files are among these files later original_files = ( - [file for file in domain_path.iterdir() if Domain.is_domain_file(file)] + { + file: rasa.shared.utils.io.read_yaml_file(file) + for file in domain_path.iterdir() + if Domain.is_domain_file(file) + } if domain_path.is_dir() - else [domain_path] + else {domain_path: rasa.shared.utils.io.read_yaml_file(domain_path)} ) - migrated_files = [ - file - for file in original_files - if rasa.shared.utils.io.read_yaml_file(file).get("version") == "3.0" - ] + migrated_files = [] + + for file, file_dict in original_files.items(): + if not isinstance(file_dict, dict): + raise RasaException( + f"The file {file} could not be read " + f"as an eligible domain dictionary. " + f"Please make sure you have included " + f"only eligible domain files." + ) + + if file_dict.get(KEY_TRAINING_DATA_FORMAT_VERSION) == "3.0": + migrated_files.append(file) + if migrated_files: raise RasaException( f"Some of the given files ({[file for file in migrated_files]}) " diff --git a/rasa/core/nlg/response.py b/rasa/core/nlg/response.py index 2da05d2130af..4db27427e147 100644 --- a/rasa/core/nlg/response.py +++ b/rasa/core/nlg/response.py @@ -30,7 +30,7 @@ def _matches_filled_slots( self, filled_slots: Dict[Text, Any], response: Dict[Text, Any] ) -> bool: """Checks if the conditional response variation matches the filled slots.""" - constraints = response.get(RESPONSE_CONDITION) + constraints = response.get(RESPONSE_CONDITION, []) for constraint in constraints: name = constraint["name"] value = constraint["value"] diff --git a/rasa/core/policies/memoization.py b/rasa/core/policies/memoization.py index 405578e25f52..9618da6a28db 100644 --- a/rasa/core/policies/memoization.py +++ b/rasa/core/policies/memoization.py @@ -213,7 +213,10 @@ def _prediction_result( ) -> List[float]: result = self._default_predictions(domain) if action_name: - if self.config["use_nlu_confidence_as_score"]: + if ( + self.config["use_nlu_confidence_as_score"] + and tracker.latest_message is not None + ): # the memoization will use the confidence of NLU on the latest # user message to set the confidence of the action score = tracker.latest_message.intent.get("confidence", 1.0) diff --git a/rasa/core/policies/rule_policy.py b/rasa/core/policies/rule_policy.py index b16884baa5c2..83cbe066b4fa 100644 --- a/rasa/core/policies/rule_policy.py +++ b/rasa/core/policies/rule_policy.py @@ -498,8 +498,10 @@ def _collect_sources( # we need to remember which action should be predicted by the rule # in order to correctly output the names of the contradicting rules rule_name = tracker.sender_id - if prediction_source.startswith(DEFAULT_RULES) or prediction_source.startswith( - LOOP_RULES + + if prediction_source is not None and ( + prediction_source.startswith(DEFAULT_RULES) + or prediction_source.startswith(LOOP_RULES) ): # the real gold action contradict the one in the rules in this case gold_action_name = predicted_action_name diff --git a/rasa/core/policies/ted_policy.py b/rasa/core/policies/ted_policy.py index 3c6539d41636..a6f10bf1b948 100644 --- a/rasa/core/policies/ted_policy.py +++ b/rasa/core/policies/ted_policy.py @@ -12,6 +12,7 @@ from rasa.engine.graph import ExecutionContext from rasa.engine.storage.resource import Resource from rasa.engine.storage.storage import ModelStorage +from rasa.exceptions import ModelNotFound from rasa.nlu.constants import TOKENS_NAMES from rasa.nlu.extractors.extractor import EntityTagSpec, EntityExtractorMixin import rasa.core.actions.action @@ -412,7 +413,11 @@ def _create_label_data( ) -> Tuple[RasaModelData, List[Dict[Text, List[Features]]]]: # encode all label_ids with policies' featurizer state_featurizer = self.featurizer.state_featurizer - encoded_all_labels = state_featurizer.encode_all_labels(domain, precomputations) + encoded_all_labels = ( + state_featurizer.encode_all_labels(domain, precomputations) + if state_featurizer is not None + else [] + ) attribute_data, _ = convert_to_data_format( encoded_all_labels, featurizers=self.config[FEATURIZERS] @@ -616,7 +621,11 @@ def _prepare_for_training( ) if self.config[ENTITY_RECOGNITION]: - self._entity_tag_specs = self.featurizer.state_featurizer.entity_tag_specs + self._entity_tag_specs = ( + self.featurizer.state_featurizer.entity_tag_specs + if self.featurizer.state_featurizer is not None + else [] + ) # keep one example for persisting and loading self.data_example = model_data.first_data_example() @@ -665,6 +674,10 @@ def run_training( self.config[TENSORBOARD_LOG_LEVEL], self.tmp_checkpoint_dir, ) + + if self.model is None: + raise ModelNotFound("No model was detected prior to training.") + self.model.fit( data_generator, epochs=self.config[EPOCHS], @@ -876,7 +889,7 @@ def _create_optional_event_for_entities( # entities belong to the last message of the tracker # convert the predicted tags to actual entities - text = tracker.latest_message.text + text = tracker.latest_message.text if tracker.latest_message is not None else "" if precomputations is not None: parsed_message = precomputations.lookup_message(user_text=text) else: @@ -944,7 +957,8 @@ def persist_model_utilities(self, model_path: Path) -> None: model_path / f"{model_filename}.fake_features.pkl", self.fake_features ) rasa.utils.io.pickle_dump( - model_path / f"{model_filename}.label_data.pkl", dict(self._label_data.data) + model_path / f"{model_filename}.label_data.pkl", + dict(self._label_data.data) if self._label_data is not None else {}, ) entity_tag_specs = ( [tag_spec._asdict() for tag_spec in self._entity_tag_specs] diff --git a/rasa/core/policies/unexpected_intent_policy.py b/rasa/core/policies/unexpected_intent_policy.py index eae372ade64e..1990a6e1993e 100644 --- a/rasa/core/policies/unexpected_intent_policy.py +++ b/rasa/core/policies/unexpected_intent_policy.py @@ -421,7 +421,11 @@ def compute_label_quantiles_post_training( # Hence, we first filter out the attributes inside `model_data` # to keep only those which should be present during prediction. model_prediction_data = self._prepare_data_for_prediction(model_data) - prediction_scores = self.model.run_bulk_inference(model_prediction_data) + prediction_scores = ( + self.model.run_bulk_inference(model_prediction_data) + if self.model is not None + else {} + ) label_id_scores = self._collect_label_id_grouped_scores( prediction_scores, label_ids ) @@ -608,7 +612,12 @@ def predict_action_probabilities( sequence_similarities = all_similarities[:, -1, :] # Check for unlikely intent - query_intent = tracker.get_last_event_for(UserUttered).intent_name + last_user_uttered_event = tracker.get_last_event_for(UserUttered) + query_intent = ( + last_user_uttered_event.intent_name + if last_user_uttered_event is not None + else "" + ) is_unlikely_intent = self._check_unlikely_intent( domain, sequence_similarities, query_intent ) diff --git a/rasa/core/processor.py b/rasa/core/processor.py index 38518141f75c..d58e1e0e7fd3 100644 --- a/rasa/core/processor.py +++ b/rasa/core/processor.py @@ -705,9 +705,10 @@ async def _handle_message_with_tracker( @staticmethod def _should_handle_message(tracker: DialogueStateTracker) -> bool: - return ( - not tracker.is_paused() - or tracker.latest_message.intent.get(INTENT_NAME_KEY) == USER_INTENT_RESTART + return not tracker.is_paused() or ( + tracker.latest_message is not None + and tracker.latest_message.intent.get(INTENT_NAME_KEY) + == USER_INTENT_RESTART ) def is_action_limit_reached( diff --git a/rasa/core/test.py b/rasa/core/test.py index bca372455508..8fabcaaf1d1d 100644 --- a/rasa/core/test.py +++ b/rasa/core/test.py @@ -434,7 +434,8 @@ def _create_data_generator( from rasa.shared.core.generator import TrainingDataGenerator tmp_domain_path = Path(tempfile.mkdtemp()) / "domain.yaml" - agent.domain.persist(tmp_domain_path) + domain = agent.domain if agent.domain is not None else Domain.empty() + domain.persist(tmp_domain_path) test_data_importer = TrainingDataImporter.load_from_dict( training_data_paths=[resource_name], domain_path=str(tmp_domain_path) ) @@ -823,14 +824,25 @@ async def _predict_tracker_actions( ]: processor = agent.processor + if agent.processor is not None: + processor = agent.processor + else: + raise RasaException( + "The agent's processor has not been instantiated. " + "The processor needs to be defined before running " + "prediction." + ) + tracker_eval_store = EvaluationStore() events = list(tracker.events) + slots = agent.domain.slots if agent.domain is not None else [] + partial_tracker = DialogueStateTracker.from_events( tracker.sender_id, events[:1], - agent.domain.slots, + slots, sender_source=tracker.sender_source, ) tracker_actions = [] diff --git a/rasa/core/tracker_store.py b/rasa/core/tracker_store.py index 54f3d06b4173..a97b0166db8a 100644 --- a/rasa/core/tracker_store.py +++ b/rasa/core/tracker_store.py @@ -85,7 +85,7 @@ def __init__( destination. kwargs: Additional kwargs. """ - self.domain = domain + self.domain = domain or Domain.empty() self.event_broker = event_broker self.max_event_history = None @@ -219,7 +219,10 @@ def retrieve_full_tracker( return self.retrieve(conversation_id) def stream_events(self, tracker: DialogueStateTracker) -> None: - """Streams events to a message broker""" + """Streams events to a message broker.""" + if self.event_broker is None: + return None + offset = self.number_of_existing_events(tracker.sender_id) events = tracker.events for event in list(itertools.islice(events, offset, len(events))): @@ -277,9 +280,8 @@ def __init__( super().__init__(domain, event_broker, **kwargs) def save(self, tracker: DialogueStateTracker) -> None: - """Updates and saves the current conversation state""" - if self.event_broker: - self.stream_events(tracker) + """Updates and saves the current conversation state.""" + self.stream_events(tracker) serialised = InMemoryTrackerStore.serialise_tracker(tracker) self.store[tracker.sender_id] = serialised @@ -354,8 +356,7 @@ def save( self, tracker: DialogueStateTracker, timeout: Optional[float] = None ) -> None: """Saves the current conversation state.""" - if self.event_broker: - self.stream_events(tracker) + self.stream_events(tracker) if not timeout and self.record_exp: timeout = self.record_exp @@ -445,8 +446,7 @@ def get_or_create_table( def save(self, tracker: DialogueStateTracker) -> None: """Saves the current conversation state.""" - if self.event_broker: - self.stream_events(tracker) + self.stream_events(tracker) serialized = self.serialise_tracker(tracker) self.db.put_item(Item=serialized) @@ -477,9 +477,12 @@ def retrieve(self, sender_id: Text) -> Optional[DialogueStateTracker]: # `float`s are stored as `Decimal` objects - we need to convert them back events_with_floats = core_utils.replace_decimals_with_floats(events) - return DialogueStateTracker.from_dict( - sender_id, events_with_floats, self.domain.slots - ) + if self.domain is None: + slots = [] + else: + slots = self.domain.slots + + return DialogueStateTracker.from_dict(sender_id, events_with_floats, slots) def keys(self) -> Iterable[Text]: """Returns sender_ids of the `DynamoTrackerStore`.""" @@ -553,8 +556,7 @@ def _current_tracker_state_without_events(tracker: DialogueStateTracker) -> Dict def save(self, tracker: DialogueStateTracker) -> None: """Saves the current conversation state.""" - if self.event_broker: - self.stream_events(tracker) + self.stream_events(tracker) additional_events = self._additional_events(tracker) @@ -679,7 +681,6 @@ def _create_sequence(table_name: Text) -> "Sequence": Returns: A `Sequence` object """ - from sqlalchemy.ext.declarative import declarative_base sequence_name = f"{table_name}_seq" @@ -1066,9 +1067,7 @@ def _event_query( def save(self, tracker: DialogueStateTracker) -> None: """Update database with events from the current conversation.""" - - if self.event_broker: - self.stream_events(tracker) + self.stream_events(tracker) with self.session_scope() as session: # only store recent events diff --git a/rasa/core/training/interactive.py b/rasa/core/training/interactive.py index 477a9cf4c84a..284c73f74710 100644 --- a/rasa/core/training/interactive.py +++ b/rasa/core/training/interactive.py @@ -958,7 +958,10 @@ async def _predict_till_next_listen( listen = False while not listen: result = await request_prediction(endpoint, conversation_id) - predictions = result.get("scores") or [] + if result is None: + result = {} + + predictions = result.get("scores", []) if not predictions: raise InvalidConfigException( "Cannot continue as no action was predicted by the dialogue manager. " @@ -1479,7 +1482,9 @@ async def record_messages( ) return - intents = [next(iter(i)) for i in (domain.get("intents") or [])] + domain_intents = domain.get("intents", []) if domain is not None else [] + + intents = [next(iter(i)) for i in domain_intents] num_messages = 0 diff --git a/rasa/engine/runner/dask.py b/rasa/engine/runner/dask.py index f2d5d7d511a3..2b17d5e2a9d6 100644 --- a/rasa/engine/runner/dask.py +++ b/rasa/engine/runner/dask.py @@ -105,6 +105,9 @@ def run( @staticmethod def _add_inputs_to_graph(inputs: Optional[Dict[Text, Any]], graph: Any) -> None: + if inputs is None: + return + for input_name, input_value in inputs.items(): if isinstance(input_value, str) and input_value in graph.keys(): raise GraphRunError( diff --git a/rasa/engine/storage/local_model_storage.py b/rasa/engine/storage/local_model_storage.py index 84c2d48d3dfd..b8dfc6b923e1 100644 --- a/rasa/engine/storage/local_model_storage.py +++ b/rasa/engine/storage/local_model_storage.py @@ -164,6 +164,9 @@ def create_model_package( model_metadata = self._create_model_metadata(domain, model_configuration) self._persist_metadata(model_metadata, temporary_directory) + if isinstance(model_archive_path, str): + model_archive_path = Path(model_archive_path) + if not model_archive_path.parent.exists(): model_archive_path.parent.mkdir(parents=True) diff --git a/rasa/engine/validation.py b/rasa/engine/validation.py index fe843b2dfb96..b57f227becdb 100644 --- a/rasa/engine/validation.py +++ b/rasa/engine/validation.py @@ -438,9 +438,8 @@ def _validate_needs( ) required_type = available_args.get(param_name) - needs_passed_to_kwargs = has_kwargs and required_type is None - if not needs_passed_to_kwargs: + if not has_kwargs and required_type is not None: parent = None if _is_placeholder_input(parent_name): parent_return_type = RESERVED_PLACEHOLDERS[parent_name] diff --git a/rasa/jupyter.py b/rasa/jupyter.py index abb40a442906..42cc9addc079 100644 --- a/rasa/jupyter.py +++ b/rasa/jupyter.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Text import asyncio +from rasa.shared.exceptions import RasaException from rasa.shared.utils.cli import print_success import rasa.core.agent import rasa.utils.common @@ -31,6 +32,12 @@ def chat( if model_path: agent = rasa.core.agent.load_agent(model_path=model_path, endpoints=endpoints) + if agent is None: + raise RasaException( + "Either the provided model path could not load the agent " + "or no core agent was provided." + ) + print("Your bot is ready to talk! Type your messages here or send '/stop'.") while True: message = input() diff --git a/rasa/nlu/classifiers/diet_classifier.py b/rasa/nlu/classifiers/diet_classifier.py index 790802f07745..d938f2438f2f 100644 --- a/rasa/nlu/classifiers/diet_classifier.py +++ b/rasa/nlu/classifiers/diet_classifier.py @@ -3,6 +3,8 @@ import logging from collections import defaultdict from pathlib import Path + +from rasa.exceptions import ModelNotFound from rasa.nlu.featurizers.featurizer import Featurizer import numpy as np @@ -666,6 +668,9 @@ def _create_label_data( return label_data def _use_default_label_features(self, label_ids: np.ndarray) -> List[FeatureArray]: + if self._label_data is None: + return [] + feature_arrays: List[FeatureArray] = self._label_data.get(LABEL, SENTENCE) all_label_features = feature_arrays[0] return [ @@ -887,6 +892,9 @@ def train(self, training_data: TrainingData) -> Resource: optimizer=tf.keras.optimizers.Adam(self.component_config[LEARNING_RATE]) ) else: + if self.model is None: + raise ModelNotFound("Model could not be found. ") + self.model.adjust_for_incremental_training( data_example=self._data_example, new_sparse_feature_sizes=model_data.get_sparse_feature_sizes(), @@ -1058,7 +1066,8 @@ def persist(self) -> None: self._sparse_feature_sizes, ) io_utils.pickle_dump( - model_path / f"{file_name}.label_data.pkl", dict(self._label_data.data) + model_path / f"{file_name}.label_data.pkl", + dict(self._label_data.data) if self._label_data is not None else {}, ) io_utils.json_pickle( model_path / f"{file_name}.index_label_id_mapping.json", diff --git a/rasa/nlu/classifiers/sklearn_intent_classifier.py b/rasa/nlu/classifiers/sklearn_intent_classifier.py index 087aae9826f6..4166ab37254a 100644 --- a/rasa/nlu/classifiers/sklearn_intent_classifier.py +++ b/rasa/nlu/classifiers/sklearn_intent_classifier.py @@ -15,6 +15,7 @@ from rasa.engine.storage.storage import ModelStorage from rasa.shared.constants import DOCS_URL_TRAINING_DATA_NLU from rasa.nlu.classifiers import LABEL_RANKING_LENGTH +from rasa.shared.exceptions import RasaException from rasa.shared.nlu.constants import TEXT from rasa.nlu.classifiers.classifier import IntentClassifier from rasa.shared.nlu.training_data.training_data import TrainingData @@ -63,7 +64,7 @@ def __init__( config: Dict[Text, Any], model_storage: ModelStorage, resource: Resource, - clf: "sklearn.model_selection.GridSearchCV" = None, + clf: Optional["sklearn.model_selection.GridSearchCV"] = None, le: Optional["sklearn.preprocessing.LabelEncoder"] = None, ) -> None: """Construct a new intent classifier using the sklearn framework.""" @@ -195,7 +196,7 @@ def _create_classifier( def process(self, messages: List[Message]) -> List[Message]: """Return the most likely intent and its probability for a message.""" for message in messages: - if not self.clf or not message.features_present( + if self.clf is None or not message.features_present( attribute=TEXT, featurizers=self.component_config.get(FEATURIZERS) ): # component is either not trained or didn't @@ -240,6 +241,11 @@ def predict_prob(self, X: np.ndarray) -> np.ndarray: :param X: bow of input text :return: vector of probabilities containing one entry for each label. """ + if self.clf is None: + raise RasaException( + "Sklearn intent classifier has not been initialised and trained." + ) + return self.clf.predict_proba(X) def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: diff --git a/rasa/nlu/extractors/mitie_entity_extractor.py b/rasa/nlu/extractors/mitie_entity_extractor.py index 8472fdb4005b..2a2705f665f4 100644 --- a/rasa/nlu/extractors/mitie_entity_extractor.py +++ b/rasa/nlu/extractors/mitie_entity_extractor.py @@ -231,9 +231,12 @@ def _extract_entities( entities = [] token_texts = [token.text for token in tokens] - mitie_entities = self._ner.extract_entities( - token_texts, mitie_model.word_feature_extractor - ) + if self._ner is None: + mitie_entities = [] + else: + mitie_entities = self._ner.extract_entities( + token_texts, mitie_model.word_feature_extractor + ) for e in mitie_entities: if len(e[0]): start = tokens[e[0][0]].start diff --git a/rasa/shared/core/conversation.py b/rasa/shared/core/conversation.py index 807464190a75..e2e4e2cb8c60 100644 --- a/rasa/shared/core/conversation.py +++ b/rasa/shared/core/conversation.py @@ -34,8 +34,7 @@ def from_parameters(cls, parameters: Dict[Text, Any]) -> "Dialogue": Deserialised `Dialogue`. """ - return cls( parameters.get("name"), - [Event.from_parameters(evt) for evt in parameters.get("events")], + [Event.from_parameters(evt) for evt in parameters.get("events", [])], ) diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index c22785b31774..de03c87e7e21 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -434,7 +434,7 @@ def _sanitize_intents_in_domain_dict(data: Dict[Text, Any]) -> Dict[Text, Any]: if not data.get(KEY_INTENTS): return data - for intent in data.get(KEY_INTENTS): + for intent in data.get(KEY_INTENTS, []): if isinstance(intent, dict): Domain._reset_intent_flags(intent) diff --git a/rasa/shared/core/events.py b/rasa/shared/core/events.py index 92fbf266b0bb..864e49efa5ef 100644 --- a/rasa/shared/core/events.py +++ b/rasa/shared/core/events.py @@ -735,6 +735,9 @@ def apply_to(self, tracker: "DialogueStateTracker") -> None: # a user message is always followed by action listen return + if not tracker.latest_message: + return + # update previous user message's featurization based on this event tracker.latest_message.use_text_for_featurization = ( self.use_text_for_featurization @@ -815,6 +818,9 @@ def apply_to(self, tracker: "DialogueStateTracker") -> None: # a user message always comes after action listen return + if not tracker.latest_message: + return + for entity in self.entities: if entity not in tracker.latest_message.entities: tracker.latest_message.entities.append(entity) diff --git a/rasa/shared/core/generator.py b/rasa/shared/core/generator.py index fa681162f363..fbef9618caf0 100644 --- a/rasa/shared/core/generator.py +++ b/rasa/shared/core/generator.py @@ -61,8 +61,8 @@ def __init__( super().__init__( sender_id, slots, max_event_history, is_rule_tracker=is_rule_tracker ) - self._states_for_hashing: Optional[Deque[FrozenState]] = None - self.domain = domain + self._states_for_hashing: Deque[FrozenState] = deque() + self.domain = domain if domain is not None else Domain.empty() # T/F property to filter augmented stories self.is_augmented = is_augmented @@ -104,7 +104,7 @@ def past_states_for_hashing( # if don't have it cached, we use the domain to calculate the states # from the events states_for_hashing = self._states_for_hashing - if states_for_hashing is None: + if not states_for_hashing: states = super().past_states(domain, omit_unset_slots=omit_unset_slots) states_for_hashing = deque(self.freeze_current_state(s) for s in states) @@ -146,7 +146,7 @@ def past_states( def clear_states(self) -> None: """Reset the states.""" - self._states_for_hashing = None + self._states_for_hashing = deque() def init_copy(self) -> "TrackerWithCachedStates": """Create a new state tracker with the same initial values.""" @@ -193,8 +193,7 @@ def update(self, event: Event, skip_states: bool = False) -> None: """Modify the state of the tracker according to an ``Event``.""" # if `skip_states` is `True`, this function behaves exactly like the # normal update of the `DialogueStateTracker` - - if self._states_for_hashing is None and not skip_states: + if not self._states_for_hashing and not skip_states: # rest of this function assumes we have the previous state # cached. let's make sure it is there. self._states_for_hashing = self.past_states_for_hashing(self.domain) diff --git a/rasa/shared/core/slot_mappings.py b/rasa/shared/core/slot_mappings.py index 8dfeaf724bf6..6e31bb26fa76 100644 --- a/rasa/shared/core/slot_mappings.py +++ b/rasa/shared/core/slot_mappings.py @@ -114,7 +114,10 @@ def intent_is_desired( ) ) - intent = tracker.latest_message.intent.get(INTENT_NAME_KEY) + if tracker.latest_message: + intent = tracker.latest_message.intent.get(INTENT_NAME_KEY) + else: + intent = None intent_not_blocked = not mapping_intents and intent not in mapping_not_intents @@ -145,7 +148,10 @@ def entity_is_desired( True, if slot should be filled, false otherwise. """ slot_fulfils_entity_mapping = False - extracted_entities = tracker.latest_message.entities + if tracker.latest_message: + extracted_entities = tracker.latest_message.entities + else: + extracted_entities = [] for entity in extracted_entities: if ( diff --git a/rasa/shared/core/trackers.py b/rasa/shared/core/trackers.py index 5f4448371a7c..f2116aa2e67f 100644 --- a/rasa/shared/core/trackers.py +++ b/rasa/shared/core/trackers.py @@ -404,6 +404,8 @@ def get_latest_entity_values( Returns: Entity values. """ + if self.latest_message is None: + return iter([]) return ( x.get(ENTITY_ATTRIBUTE_VALUE) diff --git a/rasa/shared/core/training_data/story_reader/story_reader.py b/rasa/shared/core/training_data/story_reader/story_reader.py index 6d2a32785f49..850c140ea2e8 100644 --- a/rasa/shared/core/training_data/story_reader/story_reader.py +++ b/rasa/shared/core/training_data/story_reader/story_reader.py @@ -91,6 +91,8 @@ def _parse_events( def _add_event(self, event_name: Text, parameters: Dict[Text, Any]) -> None: parsed_events = self._parse_events(event_name, parameters) + if parsed_events is None: + parsed_events = [] if self.current_step_builder is None: raise StoryParseError( diff --git a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py index 4eaeaef6bb52..aaba8ef17b4f 100644 --- a/rasa/shared/core/training_data/story_reader/yaml_story_reader.py +++ b/rasa/shared/core/training_data/story_reader/yaml_story_reader.py @@ -321,7 +321,8 @@ def _parse_user_utterance(self, step: Dict[Text, Any]) -> None: else: self._validate_that_utterance_is_in_domain(utterance) - self.current_step_builder.add_user_messages([utterance]) + if self.current_step_builder is not None: + self.current_step_builder.add_user_messages([utterance]) def _validate_that_utterance_is_in_domain(self, utterance: UserUttered) -> None: intent_name = utterance.intent.get(INTENT_NAME_KEY) @@ -347,7 +348,7 @@ def _validate_that_utterance_is_in_domain(self, utterance: UserUttered) -> None: def _parse_or_statement(self, step: Dict[Text, Any]) -> None: events: List = [] - for item in step.get(KEY_OR): + for item in step.get(KEY_OR, []): if KEY_USER_INTENT in item.keys(): utterance = self._parse_raw_user_utterance(item) if utterance: @@ -385,7 +386,7 @@ def _parse_or_statement(self, step: Dict[Text, Any]) -> None: ) return - if events: + if events and self.current_step_builder is not None: self.current_step_builder.add_events(events) def _user_intent_from_step( diff --git a/rasa/shared/core/training_data/structures.py b/rasa/shared/core/training_data/structures.py index 685b6f024d52..ae42a0980212 100644 --- a/rasa/shared/core/training_data/structures.py +++ b/rasa/shared/core/training_data/structures.py @@ -598,7 +598,7 @@ def _remove_unused_generated_cps( unused_genr_cps = { cp_name for cp_name in unused_cps - if cp_name.startswith(GENERATED_CHECKPOINT_PREFIX) + if cp_name is not None and cp_name.startswith(GENERATED_CHECKPOINT_PREFIX) } k_to_remove = set() diff --git a/rasa/shared/core/training_data/visualization.py b/rasa/shared/core/training_data/visualization.py index e22dc03e3ed4..e3eaa5bdfedd 100644 --- a/rasa/shared/core/training_data/visualization.py +++ b/rasa/shared/core/training_data/visualization.py @@ -335,14 +335,15 @@ def _length_of_common_action_prefix(this: List[Event], other: List[Event]) -> in ) for i, e in enumerate(t_cleaned): + o = o_cleaned[i] if i == len(o_cleaned): break - elif isinstance(e, UserUttered) and isinstance(o_cleaned[i], UserUttered): + elif isinstance(e, UserUttered) and isinstance(o, UserUttered): continue elif ( isinstance(e, ActionExecuted) - and isinstance(o_cleaned[i], ActionExecuted) - and o_cleaned[i].action_name == e.action_name + and isinstance(o, ActionExecuted) + and o.action_name == e.action_name ): num_common_actions += 1 else: @@ -473,11 +474,14 @@ def visualize_neighborhood( and event_idx.action_name == ACTION_LISTEN_NAME ): next_node_idx += 1 + if message is None: + label = " ? " + else: + intent = cast(dict, message).get("intent", {}) + label = intent.get("name", " ? ") graph.add_node( next_node_idx, - label=" ? " - if not message - else message.get("intent", {}).get("name", " ? "), + label=label, shape="rect", **{"class": "intent dashed active"}, ) diff --git a/rasa/shared/nlu/training_data/loading.py b/rasa/shared/nlu/training_data/loading.py index 335f023d429a..163303fa5cf4 100644 --- a/rasa/shared/nlu/training_data/loading.py +++ b/rasa/shared/nlu/training_data/loading.py @@ -2,7 +2,7 @@ import logging import os import typing -from typing import Optional, Text, Callable, Dict, Any +from typing import Optional, Text, Callable, Dict, Any, List import rasa.shared.utils.io from rasa.shared.nlu.training_data.formats.dialogflow import ( @@ -54,13 +54,13 @@ def load_data(resource_name: Text, language: Optional[Text] = "en") -> "Training files = rasa.shared.utils.io.list_files(resource_name) data_sets = [_load(f, language) for f in files] - data_sets = [ds for ds in data_sets if ds] - if len(data_sets) == 0: + training_data_sets: List[TrainingData] = [ds for ds in data_sets if ds] + if len(training_data_sets) == 0: training_data = TrainingData() - elif len(data_sets) == 1: - training_data = data_sets[0] + elif len(training_data_sets) == 1: + training_data = training_data_sets[0] else: - training_data = data_sets[0].merge(*data_sets[1:]) + training_data = training_data_sets[0].merge(*data_sets[1:]) return training_data diff --git a/rasa/telemetry.py b/rasa/telemetry.py index 107d0de0f03c..592f0ec10512 100644 --- a/rasa/telemetry.py +++ b/rasa/telemetry.py @@ -986,10 +986,15 @@ def track_core_model_test(num_story_steps: int, e2e: bool, agent: "Agent") -> No e2e: indicator if tests running in end to end mode agent: Agent of the model getting tested """ + if agent.processor is None: + project_fingerprint = "" + else: + project_fingerprint = agent.processor.model_metadata.project_fingerprint + _track( TELEMETRY_TEST_CORE_EVENT, { - "project": agent.processor.model_metadata.project_fingerprint, + "project": project_fingerprint, "end_to_end": e2e, "num_story_steps": num_story_steps, }, diff --git a/rasa/utils/tensorflow/model_data_utils.py b/rasa/utils/tensorflow/model_data_utils.py index cba475d8346a..7e3b2b794e94 100644 --- a/rasa/utils/tensorflow/model_data_utils.py +++ b/rasa/utils/tensorflow/model_data_utils.py @@ -50,6 +50,8 @@ def featurize_training_examples( A dictionary of attribute to feature sizes. """ output = [] + if not entity_tag_specs: + entity_tag_specs = [] for example in training_examples: attribute_to_features = {} diff --git a/rasa/utils/tensorflow/rasa_layers.py b/rasa/utils/tensorflow/rasa_layers.py index 8b68eceaa167..4d06a9d78986 100644 --- a/rasa/utils/tensorflow/rasa_layers.py +++ b/rasa/utils/tensorflow/rasa_layers.py @@ -127,8 +127,7 @@ def _replace_dense_for_sparse_layer( """ kernel = layer_to_replace.get_kernel().numpy() bias = layer_to_replace.get_bias() - use_bias = False if bias is None else True - if use_bias: + if bias is not None: bias = bias.numpy() units = layer_to_replace.get_units() # split kernel by feature sizes to update the layer accordingly @@ -155,12 +154,12 @@ def _replace_dense_for_sparse_layer( # stack each merged weight to form a new weight tensor new_weights = np.vstack(merged_weights) kernel_init = tf.constant_initializer(new_weights) - bias_init = tf.constant_initializer(bias) if use_bias else None + bias_init = tf.constant_initializer(bias) if bias is not None else None new_layer = layers.DenseForSparse( name=f"sparse_to_dense.{attribute}_{feature_type}", reg_lambda=reg_lambda, units=units, - use_bias=use_bias, + use_bias=bias is not None, kernel_initializer=kernel_init, bias_initializer=bias_init, ) diff --git a/rasa/utils/tensorflow/temp_keras_modules.py b/rasa/utils/tensorflow/temp_keras_modules.py index 57a605c38552..cb9c40b05176 100644 --- a/rasa/utils/tensorflow/temp_keras_modules.py +++ b/rasa/utils/tensorflow/temp_keras_modules.py @@ -1,5 +1,5 @@ import copy -from typing import List, Dict, Union, Optional, Any, Generator, Tuple, Iterator +from typing import List, Dict, Union, Optional, Any, Generator, Tuple, Iterator, cast import numpy as np @@ -358,11 +358,12 @@ def fit( epochs=epochs, steps=data_handler.inferred_steps, ) + callbacks_list = cast(callbacks_module.CallbackList, callbacks) self.stop_training = False self.train_function = self.make_train_function() self._train_counter.assign(0) - callbacks.on_train_begin() + callbacks_list.on_train_begin() training_logs = None # Handle fault-tolerance for multi-worker. # TODO(omalleyt): Fix the ordering issues that mean this has to @@ -373,7 +374,7 @@ def fit( logs = None for epoch, iterator in data_handler.enumerate_epochs(): self.reset_metrics() - callbacks.on_epoch_begin(epoch) + callbacks_list.on_epoch_begin(epoch) with data_handler.catch_stop_iteration(): for step in data_handler.steps(): with tf.profiler.experimental.Trace( @@ -383,13 +384,13 @@ def fit( batch_size=batch_size, _r=1, ): - callbacks.on_train_batch_begin(step) + callbacks_list.on_train_batch_begin(step) tmp_logs = self.train_function(iterator) if data_handler.should_sync: context.async_wait() logs = tmp_logs # No error, now safe to assign to logs. end_step = step + data_handler.step_increment - callbacks.on_train_batch_end(end_step, logs) + callbacks_list.on_train_batch_end(end_step, logs) if self.stop_training: break @@ -429,7 +430,7 @@ def fit( sample_weight=val_sample_weight, batch_size=validation_batch_size or batch_size, steps=validation_steps, - callbacks=callbacks, + callbacks=callbacks_list, max_queue_size=max_queue_size, workers=workers, use_multiprocessing=use_multiprocessing, @@ -439,7 +440,7 @@ def fit( val_logs = {"val_" + name: val for name, val in val_logs.items()} epoch_logs.update(val_logs) - callbacks.on_epoch_end(epoch, epoch_logs) + callbacks_list.on_epoch_end(epoch, epoch_logs) training_logs = epoch_logs if self.stop_training: break @@ -447,7 +448,7 @@ def fit( # If eval_data_handler exists, delete it after all epochs are done. if getattr(self, "_eval_data_handler", None) is not None: del self._eval_data_handler - callbacks.on_train_end(logs=training_logs) + callbacks_list.on_train_end(logs=training_logs) return self.history diff --git a/rasa/validator.py b/rasa/validator.py index 30b67ab66a54..36d160d651e3 100644 --- a/rasa/validator.py +++ b/rasa/validator.py @@ -184,6 +184,10 @@ def verify_utterances_in_stories(self, ignore_warnings: bool = True) -> bool: for event in story.events: if not isinstance(event, ActionExecuted): continue + + if not event.action_name: + continue + if not event.action_name.startswith(UTTER_PREFIX): # we are only interested in utter actions continue @@ -253,6 +257,9 @@ def verify_actions_in_stories_rules(self) -> bool: if not isinstance(event, ActionExecuted): continue + if not event.action_name: + continue + if not event.action_name.startswith("action_"): continue diff --git a/setup.cfg b/setup.cfg index d8d9a24dd75b..b52f945874f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,5 +47,4 @@ disallow_untyped_decorators = True # FIXME: working our way towards removing these # see https://github.com/RasaHQ/rasa/pull/6470 # the list below is sorted by the number of errors for each error code, in decreasing order -disable_error_code = arg-type, assignment, var-annotated, union-attr, - override, misc +disable_error_code = arg-type, assignment, var-annotated, override, misc From 0c76f7d097253515720ca8a415eb9f5fa88ed3e8 Mon Sep 17 00:00:00 2001 From: m-vdb Date: Thu, 3 Mar 2022 17:37:46 +0100 Subject: [PATCH 62/65] fix _migrate_domain_files() entities default --- rasa/core/migrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rasa/core/migrate.py b/rasa/core/migrate.py index e28d727223a2..fcc8f1998a67 100644 --- a/rasa/core/migrate.py +++ b/rasa/core/migrate.py @@ -264,7 +264,7 @@ def _migrate_domain_files( slots.update(original_content.get(KEY_SLOTS, {})) forms.update(original_content.get(KEY_FORMS, {})) - entities.extend(original_content.get(KEY_ENTITIES, {})) + entities.extend(original_content.get(KEY_ENTITIES, [])) if not slots or not forms: raise RasaException( From f984f3ccacccdd4cebe0f0a1da570d23b82fa40f Mon Sep 17 00:00:00 2001 From: m-vdb Date: Thu, 3 Mar 2022 17:41:19 +0100 Subject: [PATCH 63/65] add changelog entry --- changelog/9094.misc.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/9094.misc.md diff --git a/changelog/9094.misc.md b/changelog/9094.misc.md new file mode 100644 index 000000000000..10f29fcacc3c --- /dev/null +++ b/changelog/9094.misc.md @@ -0,0 +1 @@ +Enable `mypy` `var-annotated` check and fix any resulting errors. From 208c6b859197a735f743dbc1534cec1528d58a70 Mon Sep 17 00:00:00 2001 From: Melinda Loubser <32034278+melindaloubser1@users.noreply.github.com> Date: Mon, 7 Mar 2022 12:05:21 +0100 Subject: [PATCH 64/65] Backport deploy docs changes (#10957) * Backport updates to the deployment docs * Move deployment docs up to the same level as "Building Assistants" * Clarify the role of REI as a deployment helper tool --- CHANGELOG.mdx | 2 +- changelog/10957.doc.md | 1 + docs/docs/deploy/deploy-action-server.mdx | 17 + docs/docs/deploy/deploy-rasa-x.mdx | 18 + docs/docs/deploy/deploy-rasa.mdx | 288 ++++++++++ docs/docs/deploy/introduction.mdx | 54 ++ docs/docs/deploy/rei/deploy.mdx | 50 ++ docs/docs/docker/building-in-docker.mdx | 5 +- .../docker/deploying-in-docker-compose.mdx | 5 +- docs/docs/how-to-deploy.mdx | 520 ------------------ docs/docs/setting-up-ci-cd.mdx | 2 +- docs/docs/tracker-stores.mdx | 2 +- docs/sidebars.js | 21 +- 13 files changed, 454 insertions(+), 531 deletions(-) create mode 100644 changelog/10957.doc.md create mode 100644 docs/docs/deploy/deploy-action-server.mdx create mode 100644 docs/docs/deploy/deploy-rasa-x.mdx create mode 100644 docs/docs/deploy/deploy-rasa.mdx create mode 100644 docs/docs/deploy/introduction.mdx create mode 100644 docs/docs/deploy/rei/deploy.mdx delete mode 100644 docs/docs/how-to-deploy.mdx diff --git a/CHANGELOG.mdx b/CHANGELOG.mdx index 7e7c09ecf9fb..361deeb39406 100644 --- a/CHANGELOG.mdx +++ b/CHANGELOG.mdx @@ -3031,7 +3031,7 @@ https://github.com/RasaHQ/rasa/tree/main/changelog/ . --> `1.8.0`. Since version `1.8.0` the Rasa SDK Docker images does not longer run as `root` user by default. For commands which require `root` user usage, you have to switch back to the `root` user in your Docker image as described in - [Building an Action Server Image](./how-to-deploy.mdx#building-an-action-server-image). + [Building an Action Server Image](https://rasa.com/docs/action-server/deploy-action-server#building-an-action-server-image). * [#5402](https://github.com/RasaHQ/rasa/issues/5402): Made improvements to Building Assistants tutorial diff --git a/changelog/10957.doc.md b/changelog/10957.doc.md new file mode 100644 index 000000000000..d6e92c3a8f87 --- /dev/null +++ b/changelog/10957.doc.md @@ -0,0 +1 @@ +Backport the updated deployment docs to 3.0.x. diff --git a/docs/docs/deploy/deploy-action-server.mdx b/docs/docs/deploy/deploy-action-server.mdx new file mode 100644 index 000000000000..fbe94a4ae916 --- /dev/null +++ b/docs/docs/deploy/deploy-action-server.mdx @@ -0,0 +1,17 @@ +--- +id: deploy-action-server +sidebar_label: "Deploy Action Server" +title: "Deploy Action Server" +description: Deploy and connect to your custom action server +abstract: This page shows you where to find how to deploy Rasa Action Server and how to build a custom Docker image. +--- + +import variables from './../variables.json'; + +## a. Deploy Action Server + +Visit the [Rasa Action Server docs](https://rasa.com/docs/action-server/deploy-action-server#a-installation) to learn how to [build an Action Server image](https://rasa.com/docs/action-server/deploy-action-server#building-an-action-server-image) and how to deploy an Action Server using Helm. + +## b. Connect Rasa Action Server with Rasa Open Source deployment + +Visit the [Connect Rasa Action Server with Rasa Open Source deployment](https://rasa.com/docs/action-server/deploy-action-server#b-connect-rasa-action-server-with-rasa-open-source-deployment) section to learn how to connect a Rasa Action Server deployment with Rasa Open Source deployment. diff --git a/docs/docs/deploy/deploy-rasa-x.mdx b/docs/docs/deploy/deploy-rasa-x.mdx new file mode 100644 index 000000000000..71d23610eb17 --- /dev/null +++ b/docs/docs/deploy/deploy-rasa-x.mdx @@ -0,0 +1,18 @@ +--- +id: deploy-rasa-x +sidebar_label: "Deploy Rasa X" +title: "Deploy Rasa X" +description: "Deploying Rasa X to improve your Rasa assistant" +abstract: This page shows you where to find out how to deploy Rasa X and forward events to it from your Rasa Open Source deployment. +--- + +import variables from './../variables.json'; + +## a. Deploy Rasa X + +Visit the [Installation Guide for Rasa X](https://rasa.com/docs/rasa-x/installation-and-setup/installation-guide) where you can learn about available installation methods and +how to deploy Rasa X by following one of them. + +## b. Connect Rasa Open Source deployment to Rasa X + +Visit the [Connect Rasa Open Source deployment](https://rasa.com/docs/rasa-x/installation-and-setup/deploy#2-connect-rasa-open-source-deployment-the-rasa-helm-chart) section to learn how to connect your Rasa Assistant to Rasa X deployment. diff --git a/docs/docs/deploy/deploy-rasa.mdx b/docs/docs/deploy/deploy-rasa.mdx new file mode 100644 index 000000000000..ce217a9a4c0f --- /dev/null +++ b/docs/docs/deploy/deploy-rasa.mdx @@ -0,0 +1,288 @@ +--- +id: deploy-rasa +sidebar_label: "Deploy Rasa Open Source" +title: "Deploy Rasa Open Source" +description: Deploy a Rasa assistant on Kubernetes/Openshift using Helm +abstract: This page explains how to deploy Rasa Open Source using Helm. +--- + +import variables from './../variables.json'; + +:::note +The Rasa Helm chart is open source and available in the +[helm-charts repository](https://github.com/rasahq/helm-charts). +Please +[create an issue](https://github.com/RasaHQ/helm-charts/issues/new) in this +repository if you discover bugs or have suggestions for improvements. + +::: + + +## Installation Requirements + +1. Check that you have installed the Kubernetes or OpenShift command line + interface (CLI). You can check this using the following command: + + + + + ```bash + kubectl version --short --client + + # The output should be similar to this + # Client Version: v1.19.11 + ``` + + + + + ```bash + oc version --client + + # The output should be similar to this + # Client Version: 4.7.13 + ``` + + + + + If this command resulted in an error, please install the + [Kubernetes CLI](https://kubernetes.io/docs/tasks/tools/install-kubectl/) or the + [OpenShift CLI](https://docs.openshift.com/container-platform/4.7/cli_reference/openshift_cli/getting-started-cli.html#installing-openshift-cli) + depending on the cluster you’re using. + +2. Make sure that the Kubernetes / OpenShift CLI is correctly connected to + your cluster. You can do so by using the following commands: + + + + + ```bash + kubectl version --short + + # The output should be similar to this + # Client Version: v1.19.11 + # Server Version: v1.19.10 + ``` + + + + + ```bash + oc version + + # The output should be similar to this + # Client Version: 4.7.13 + # Kubernetes Version: v1.20.0+df9c838 + ``` + + + + + If you get an error when executing the command, you are not connected to your + cluster. To get the command to connect to the cluster please consult your cluster’s + admin or the documentation of your cloud provider. + +3. Make sure you have the [Helm CLI](https://helm.sh/docs/intro/install/) + installed. To check this, run: + + ```bash + helm version --short + + # The output should be similar to this + # v3.6.0+g7f2df64 + ``` + + If this command leads to an error, please install the + [Helm CLI](https://helm.sh/docs/intro/install/). + + In case you are using a version `<3.5` of Helm, please update to Helm version + `>=3.5`. + +## Installation + +### 1. Create Namespace + +We recommend installing Rasa Open Source in a separate +[namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/) +to avoid interfering with existing cluster deployments. To create a new namespace +run the following command: + + + + + ```bash + kubectl create namespace + ``` + + + + + ```bash + oc create namespace + ``` + + + + +### 2. Create Values File + +Prepare an empty file called `rasa-values.yml` which will include all your custom +configuration for the installation with Helm. + +All available values you can find in [the Rasa helm chart repository](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa#values). + +:::note +The default configuration of the Rasa chart deploys a Rasa Open Source Server, downloads a model, and serves the downloaded model. +Visit [the Rasa helm chart repository](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa#quick-start) to check out more examples of configuration. + +::: + +### 3. Loading an initial model + +The first time you install Rasa, you may not have a model server available yet, or you may want an lightweight model for testing the deployment. +For this purpose, you can choose between training or downloading an initial model. By default, the Rasa chart downloads an example model from GitHub. +To use this option, you don't have to change anything. + +If you want to define an existing model to download from a URL you define instead, update your `rasa-values.yaml` with the URL according to the following configuration: + + ```yaml + applicationSettings: + initialModel: "https://github.com/RasaHQ/rasa-x-demo/blob/master/models/model.tar.gz?raw=true" + ``` +:::note +The URL for the initial model download has to point to a tar.gz file and must not require authentication. + +::: + +If you want to train an initial model you can do this by setting the `applicationSettings.trainInitialModel` to `true`. +It creates a init container that trains a model based on data located in the `/app` directory. If the `/app` directory is empty it creates a new project. +You can find an example that shows how to download data files from a git repository and train an initial model in the Rasa helm charts [examples](https://github.com/RasaHQ/helm-charts/blob/main/examples/rasa/train-model-helmfile.yaml). + +### 4. Deploy Rasa Open Source Assistant + +Run the following commands: + +```bash +# Add the repository which contains the Rasa Helm chart +helm repo add rasa https://helm.rasa.com + +# Deploy Rasa Open Source +helm install \ + --namespace \ + --values rasa-values.yml \ + \ + rasa/rasa +``` + +:::note +**OpenShift only**: If the deployment fails and `oc get events` returns +`1001 is not an allowed group spec.containers[0].securityContext.securityContext.runAsUser`, +re-run the installation command with the following values: + +```yaml +postgresql: + volumePermissions: + securityContext: + runAsUser: "auto" + securityContext: + enabled: false + shmVolume: + chmod: + enabled: false +nginx: + image: + name: nginxinc/nginx-unprivileged + port: 8080 +``` + +Then wait until the deployment is ready. If you want to check on its status, the following command +will block until the Rasa deployment is ready: + + + + + ```bash + kubectl --namespace \ + wait \ + --for=condition=available \ + --timeout=20m \ + --selector app.kubernetes.io/instance= \ + deployment + ``` + + + + + ```bash + oc --namespace \ + wait \ + --for=condition=available \ + --timeout=20m \ + --selector app.kubernetes.io/instance= \ + deployment + ``` + + + + +::: + + +### 5. Access Rasa Open Source Assistant + +By default the Rasa deployment is exposed via the `rasa` (``) service and accessible only within a Kubernetes cluster. +To access Rasa Open Source Assistant by using `kubectl port-forward`, use these commands: + + + + + ```bash + export SERVICE_PORT=$(kubectl get --namespace -o jsonpath="{.spec.ports[0].port}" services ) + kubectl port-forward --namespace svc/ ${SERVICE_PORT}:${SERVICE_PORT} & + ``` + + + + + ```bash + export SERVICE_PORT=$(oc get --namespace -o jsonpath="{.spec.ports[0].port}" services ) + oc port-forward --namespace svc/ ${SERVICE_PORT}:${SERVICE_PORT} & + ``` + + + + +You can then access the deployment on `http://127.0.0.1:${SERVICE_PORT}` + +The other option is to expose your deployment on `NodePort` and access it directly. + +1. Prepare configuration that switch the rasa service to `NodePort`. + + ```yaml + # rasa-values.yaml + service: + type: "NodePort" + ``` + +2. Upgrade deployment. + + ```text + helm upgrade --namespace --reuse-values -f rasa-values.yaml rasa/rasa + ``` + +3. Get the node port and address for the rasa service + + ```text + export NODE_PORT=$(kubectl get --namespace -o jsonpath="{.spec.ports[0].nodePort}" services ) + + $ curl http://127.0.0.1:${NODE_PORT} + Hello from Rasa: 2.8.7 + ``` + +Visit [the Rasa helm chart README](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa#exposing-the-rasa-deployment-to-the-public) to learn other ways to expose your deployment. + +## Next Steps + +- Visit [the Rasa helm chart repository](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa) where you can find examples of configuration +- Visit [the Rasa X docs](https://rasa.com/docs/rasa-x/) and learn how to [integrate your Rasa Open Source deployment with Rasa X](https://rasa.com/docs/rasa-x/installation-and-setup/deploy#2-connect-rasa-open-source-deployment-the-rasa-helm-chart). diff --git a/docs/docs/deploy/introduction.mdx b/docs/docs/deploy/introduction.mdx new file mode 100644 index 000000000000..0d170c6bf01f --- /dev/null +++ b/docs/docs/deploy/introduction.mdx @@ -0,0 +1,54 @@ +--- +id: introduction +sidebar_label: Introduction +title: Deploying a Rasa Assistant +description: How to deploy your Rasa Assistant with Kubernetes/Openshift +abstract: This section explains when and how to deploy an assistant built with Rasa. + It will allow you to make your assistant available to users and set you up with a production-ready environment. +--- + +import variables from './../variables.json'; + +:::note +Are you unfamiliar with Docker, Kubernetes and Helm? Check out "[Understanding Rasa Deployments](https://www.youtube.com/watch?v=aAs_RS0ueEw&list=PL75e0qA87dlHmfmu7oPPYA22fmc6GJ2aW)" on our [YouTube channel](https://www.youtube.com/channel/UCJ0V6493mLvqdiVwOKWBODQ). +::: + +## When to Deploy Your Assistant + +The best time to deploy your assistant and make it available to test users is once it can handle the most +important happy paths or is what we call a [minimum viable assistant](../glossary.mdx). Then you can use incoming +conversations to inform further development of your assistant. + +Connecting your deployed assistant to Rasa X makes it easy to share your assistant +with test users via the [share your assistant feature in +Rasa X](https://rasa.com/docs/rasa-x/user-guide/share-assistant#share-your-bot). +Then, when you're ready to make your assistant available via one or more [Messaging and Voice Channels](../messaging-and-voice-channels.mdx), +you can add them to your existing deployment set up. +See the [Rasa X Installation Guide](https://rasa.com/docs/rasa-x/installation-and-setup/installation-guide/) to learn how to deploy Rasa X and connect it to your Rasa Open Source deployment. + + +## Recommended Deployment Method +The [Rasa Helm chart](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa) is the production ready method to deploy +your assistant on a Kubernetes or Openshift cluster. For details, see the [deployment instructions](./deploy-rasa.mdx). + +### Cluster Requirements + +To install the Rasa Helm chart, you need an existing +[Kubernetes cluster](https://kubernetes.io/) or [OpenShift cluster](https://www.openshift.com/). +If you don't have one yet, you can get a managed cluster from a cloud provider like: +* [Google Cloud](https://cloud.google.com/kubernetes-engine), +* [DigitalOcean](https://www.digitalocean.com/products/kubernetes/), +* [Microsoft Azure](https://azure.microsoft.com/en-us/services/kubernetes-service/), or +* [Amazon EKS](https://aws.amazon.com/eks/). + +If you are looking for a lightweight, non-production cluster that can run on a single machine, check out the instructions for using [Rasa Ephemeral Installer (REI)](../deploy/rei/deploy.mdx). REI will help you set up a local Kubernetes cluster on which you can deploy your assistant using the Rasa Helm chart. + +## Alternative Deployment Methods + +The following deployment methods are not suited to a production deployment, but can be useful for development and testing: + +* [Running an assistant locally on the command line](../command-line-interface.mdx#rasa-run) + +* [Developing an assistant in a Docker container](../docker/building-in-docker.mdx) + +* [Deploying an assistant with Docker Compose](../docker/deploying-in-docker-compose.mdx) diff --git a/docs/docs/deploy/rei/deploy.mdx b/docs/docs/deploy/rei/deploy.mdx new file mode 100644 index 000000000000..7743c90a1bd5 --- /dev/null +++ b/docs/docs/deploy/rei/deploy.mdx @@ -0,0 +1,50 @@ +--- +id: using-rei +sidebar_label: Set up a Local Cluster using Rasa Ephemeral Installer (REI) +title: Set up a Local Cluster using Rasa Ephemeral Installer (REI) +description: Learn how set up a local Kubernetes cluster using Rasa Ephemeral Installer (REI) +--- +import useBaseUrl from '@docusaurus/useBaseUrl'; + + +import variables from '../../variables.json'; + +:::note +Are you unfamiliar with Docker, Kubernetes and Helm? Check out "[Understanding Rasa Deployments](https://www.youtube.com/watch?v=aAs_RS0ueEw&list=PL75e0qA87dlHmfmu7oPPYA22fmc6GJ2aW)" on our [YouTube channel](https://www.youtube.com/channel/UCJ0V6493mLvqdiVwOKWBODQ). +::: + +If you would like to deploy Rasa Assistant using the [Rasa OSS Helm chart](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa) on your machine, +you can use Rasa Ephemeral Installer which installs all tools and creates a local Kubernetes cluster that allows you to use the [Rasa OSS Helm chart](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa). + +We recommend this method as an alternative to docker-compose. + +## Create a local Kubernetes cluster via REI + +The Rasa Ephemeral Install installs the following tools and creates a local Kubernetes cluster using `kind`. + +Tools installed by [REI](https://github.com/RasaHQ/REI): + +- [docker](https://www.docker.com/) +- [kind](https://kind.sigs.k8s.io/) +- [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) +- [helm](https://helm.sh/) +- [rasactl](https://github.com/RasaHQ/rasactl) + +1. Simply execute the following command. + + ```text + curl -O https://rei.rasa.com/rei.sh && bash rei.sh -y + ``` + + After a few minutes, all components should be installed and a local Kubernetes cluster created. + + :::tip + + You can use the `kubectl cluster-info` to verify if all is good. + + ::: + + +## Next steps + +Follow the Kubernetes instructions for [deploying using the Rasa Helm Chart](../deploy-rasa.mdx#installation). diff --git a/docs/docs/docker/building-in-docker.mdx b/docs/docs/docker/building-in-docker.mdx index 616fab756527..dd8002c2c7df 100644 --- a/docs/docs/docker/building-in-docker.mdx +++ b/docs/docs/docker/building-in-docker.mdx @@ -11,7 +11,7 @@ import variables from '../variables.json'; If you don't have a Rasa project yet, you can build one in Docker without having to install Rasa Open Source on your local machine. If you already have a model you're satisfied with, see -[Deploying Your Rasa Assistant](../how-to-deploy.mdx) to learn how to deploy your model. +[Deploying a Rasa Assistant](../deploy/introduction.mdx) to learn how to deploy your model. ## Installing Docker @@ -261,5 +261,4 @@ docker rm action-server Work on your bot until you have a minimum viable assistant that can handle your happy paths. After that, you'll want to deploy your model to get feedback from real test users. To do so, you can deploy the -model you created with Rasa X via one of our [recommended deployment methods](../how-to-deploy.mdx#recommended-deployment-methods). -Or, you can do a [Rasa-only deployment in Docker Compose](./deploying-in-docker-compose.mdx). +model you created via one of our [recommended deployment methods](../deploy/introduction.mdx#recommended-deployment-method). diff --git a/docs/docs/docker/deploying-in-docker-compose.mdx b/docs/docs/docker/deploying-in-docker-compose.mdx index 27cdbdde57db..fa30ba39811d 100644 --- a/docs/docs/docker/deploying-in-docker-compose.mdx +++ b/docs/docs/docker/deploying-in-docker-compose.mdx @@ -7,9 +7,6 @@ description: Use Docker Compose to deploy a Rasa Open Source assistant import variables from '../variables.json'; -If you would like to deploy your assistant without Rasa X, you can do so by deploying it in Docker Compose. -To deploy Rasa X and your assistant together, see the [Recommended Deployment Methods](../how-to-deploy.mdx#recommended-deployment-methods). - ## Installing Docker If you're not sure if you have Docker installed, you can check by running: @@ -71,7 +68,7 @@ Each container is declared as a `service` within the `docker-compose.yml`. The first service is the `rasa` service, which runs your Rasa server. To add the action server, add the image of your action server code. To learn how to deploy -an action server image, see [Building an Action Server Image](../how-to-deploy.mdx#building-an-action-server-image). +an action server image, see [Building an Action Server Image](https://rasa.com/docs/action-server/deploy-action-server#building-an-action-server-image).

    
     {`version: '3.0'
    diff --git a/docs/docs/how-to-deploy.mdx b/docs/docs/how-to-deploy.mdx
    deleted file mode 100644
    index fdfc00da89e0..000000000000
    --- a/docs/docs/how-to-deploy.mdx
    +++ /dev/null
    @@ -1,520 +0,0 @@
    ----
    -id: how-to-deploy
    -sidebar_label: Deploying Your Assistant
    -title: Deploying Your Rasa Assistant
    -description: How to deploy your Rasa Assistant with Docker Compose or Kubernetes/Openshift
    -abstract: This page explains when and how to deploy an assistant built with Rasa.
    -  It will allow you to make your assistant available to users and set you up with a production-ready environment.
    ----
    -
    -import variables from './variables.json';
    -
    -## When to Deploy Your Assistant
    -
    -The best time to deploy your assistant and make it available to test users is once it can handle the most
    -important happy paths or is what we call a [minimum viable assistant](./glossary.mdx).
    -
    -The recommended deployment methods described below make it easy to share your assistant
    -with test users via the [share your assistant feature in
    -Rasa X](https://rasa.com/docs/rasa-x/user-guide/share-assistant#share-your-bot).
    -Then, when you're ready to make your assistant available via one or more [Messaging and Voice Channels](./messaging-and-voice-channels.mdx),
    -you can easily add them to your existing deployment set up.
    -
    -## Recommended Deployment Methods
    -
    -The recommended way to deploy an assistant is using either the Server Quick-Install or Helm Chart
    -options we support. Both deploy Rasa X and your assistant. They are the easiest ways to deploy your assistant,
    -allow you to use Rasa X to view conversations and turn them into training data, and are production-ready.
    -For more details on deployment methods see the [Rasa X Installation Guide](https://rasa.com/docs/rasa-x/installation-and-setup/installation-guide/).
    -
    -### Server Quick-Install
    -
    -The Server Quick-Install script is the easiest way to deploy Rasa X and your assistant. It installs a Kubernetes
    -cluster on your machine with sensible defaults, getting you up and running in one command.
    -
    -* Default: Make sure you meet the [OS Requirements](https://rasa.com/docs/rasa-x/installation-and-setup/install/quick-install-script/#hardware-os-requirements),
    -  then run:
    -
    -  ```bash
    -  curl -s get-rasa-x.rasa.com | sudo bash
    -  ```
    -
    -* Custom: See [Customizing the Script](https://rasa.com/docs/rasa-x/installation-and-setup/customize/#server-quick-install)
    -  and the [Server Quick-Install docs](https://rasa.com/docs/rasa-x/installation-and-setup/install/quick-install-script) docs.
    -
    -### Helm Chart
    -
    -For assistants that will receive a lot of user traffic, setting up a Kubernetes or Openshift deployment via
    -our Helm charts is the best option. This provides a scalable architecture that is also straightforward to deploy.
    -However, you can also customize the Helm charts if you have specific requirements.
    -
    -* Default: Read the [Helm Chart Installation](https://rasa.com/docs/rasa-x/0.42.x/installation-and-setup/install/helm-chart) docs.
    -
    -* Custom: Read the above, as well as the [Advanced Configuration](https://rasa.com/docs/rasa-x/installation-and-setup/customize/#helm-chart)
    -  documentation, and customize the [open source Helm charts](https://github.com/RasaHQ/rasa-x-helm) to your needs.
    -
    -## Deploying a Rasa Open Source Assistant
    -
    -While the above deployment methods involve deploying an assistant with Rasa X, the following instructions describe how to deploy a Rasa Open Source server only
    -by using the [Rasa Helm Chart](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa) in a scalable cluster environment using OpenShift or Kubernetes (K8S).
    -
    -### Cluster Requirements
    -
    -To install the Rasa Helm chart, you need an existing
    -[Kubernetes cluster](https://kubernetes.io/) or [OpenShift cluster](https://www.openshift.com/).
    -Setting up a Kubernetes / OpenShift cluster can be tedious, hence we
    -recommend to get a managed cluster from a cloud provider like
    -[Google Cloud](https://cloud.google.com/kubernetes-engine),
    -[DigitalOcean](https://www.digitalocean.com/products/kubernetes/),
    -[Microsoft Azure](https://azure.microsoft.com/en-us/services/kubernetes-service/), or
    -[Amazon EKS](https://aws.amazon.com/eks/).
    -
    -:::note
    -The Rasa Helm chart is open source and available in the
    -[helm-charts repository](https://github.com/rasahq/helm-charts).
    -Please
    -[create an issue](https://github.com/RasaHQ/helm-charts/issues/new) in this
    -repository if you discover bugs or have suggestions for improvements.
    -
    -:::
    -
    -
    -### Installation Requirements
    -
    -1. Check that you have installed the Kubernetes or OpenShift command line
    -   interface (CLI). You can check this using the following command:
    -
    -   
    -     
    -
    -     ```bash
    -     kubectl version --short --client
    -
    -     # The output should be similar to this
    -     # Client Version: v1.19.11
    -     ```
    -
    -     
    -     
    -
    -     ```bash
    -     oc version --client
    -
    -     # The output should be similar to this
    -     # Client Version: 4.7.13
    -     ```
    -
    -     
    -   
    -
    -   If this command resulted in an error, please install the
    -   [Kubernetes CLI](https://kubernetes.io/docs/tasks/tools/install-kubectl/) or the
    -   [OpenShift CLI](https://docs.openshift.com/container-platform/4.7/cli_reference/openshift_cli/getting-started-cli.html#installing-openshift-cli)
    -   depending on the cluster you’re using.
    -
    -2. Make sure that the Kubernetes / OpenShift CLI is correctly connected to
    -   your cluster. You can do so by using the following commands:
    -
    -   
    -     
    -
    -     ```bash
    -     kubectl version --short
    -
    -     # The output should be similar to this
    -     # Client Version: v1.19.11
    -     # Server Version: v1.19.10
    -     ```
    -
    -     
    -     
    -
    -     ```bash
    -     oc version
    -
    -     # The output should be similar to this
    -     # Client Version: 4.7.13
    -     # Kubernetes Version: v1.20.0+df9c838
    -     ```
    -
    -     
    -   
    -
    -   If you get an error when executing the command, you are not connected to your
    -   cluster. To get the command to connect to the cluster please consult your cluster’s
    -   admin or the documentation of your cloud provider.
    -
    -3. Make sure you have the [Helm CLI](https://helm.sh/docs/intro/install/)
    -   installed. To check this, run:
    -
    -   ```bash
    -   helm version --short
    -
    -   # The output should be similar to this
    -   # v3.6.0+g7f2df64
    -   ```
    -
    -   If this command leads to an error, please install the
    -   [Helm CLI](https://helm.sh/docs/intro/install/).
    -
    -   In case you are using a version `<3.5` of Helm, please update to Helm version
    -   `>=3.5`.
    -
    -### Installation
    -
    -#### 1. Create Namespace
    -
    -We recommend installing Rasa Open Source in a separate
    -[namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
    -to avoid interfering with existing cluster deployments. To create a new namespace
    -run the following command:
    -
    -
    -  
    -
    -  ```bash
    -  kubectl create namespace 
    -  ```
    -
    -  
    -  
    -
    -  ```bash
    -  oc create namespace 
    -  ```
    -
    -  
    -
    -
    -#### 2. Create Values File
    -
    -Prepare an empty file called `rasa-values.yml` which will include all your custom
    -configuration for the installation with Helm.
    -
    -All available values you can find in [the Rasa helm chart repository](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa#values).
    -
    -:::note
    -The default configuration of the Rasa chart deploys a Rasa Open Source Server, downloads a model, and serves the downloaded model.
    -Visit [the Rasa helm chart repository](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa#quick-start) to check out more examples of configuration.
    -
    -:::
    -
    -#### 3. Loading an initial model
    -
    -The first time you install Rasa, you may not have a model server available yet, or you may want an lightweight model for testing the deployment.
    -For this purpose, you can choose between training or downloading an initial model. By default, the Rasa chart downloads an example model from GitHub.
    -To use this option, you don't have to change anything.
    -
    -If you want to define an existing model to download from a URL you define instead, update your `rasa-values.yaml` with the URL according to the following configuration:
    -
    -  ```yaml
    -  applicationSettings:
    -    initialModel: "https://github.com/RasaHQ/rasa-x-demo/blob/master/models/model.tar.gz?raw=true"
    -  ```
    -:::note
    -The URL for the initial model download has to point to a tar.gz file and must not require authentication.
    -
    -:::
    -
    -If you want to train an initial model you can do this by setting the `applicationSettings.trainInitialModel` to `true`.
    -It creates a init container that trains a model based on data located in the `/app` directory. If the `/app` directory is empty it creates a new project.
    -You can find an example that shows how to download data files from a git repository and train an initial model in the Rasa helm charts [examples](https://github.com/RasaHQ/helm-charts/blob/main/examples/rasa/train-model-helmfile.yaml).
    -
    -#### 4. Deploy Rasa Open Source Assistant
    -
    -Run the following commands:
    -
    -```bash
    -# Add the repository which contains the Rasa Helm chart
    -helm repo add rasa https://helm.rasa.com
    -
    -# Deploy Rasa Open Source
    -helm install \
    -    --namespace  \
    -    --values rasa-values.yml \
    -     \
    -    rasa/rasa
    -```
    -
    -:::note
    -**OpenShift only**: If the deployment fails and `oc get events` returns
    -`1001 is not an allowed group spec.containers[0].securityContext.securityContext.runAsUser`,
    -re-run the installation command with the following values:
    -
    -```yaml
    -postgresql:
    -  volumePermissions:
    -    securityContext:
    -      runAsUser: "auto"
    -  securityContext:
    -    enabled: false
    -  shmVolume:
    -    chmod:
    -      enabled: false
    -nginx:
    -  image:
    -    name: nginxinc/nginx-unprivileged
    -    port: 8080
    -```
    -
    -Then wait until the deployment is ready. If you want to check on its status, the following command
    -will block until the Rasa deployment is ready:
    -
    -
    -  
    -
    -  ```bash
    -  kubectl --namespace  \
    -      wait \
    -      --for=condition=available \
    -      --timeout=20m \
    -      --selector app.kubernetes.io/instance= \
    -      deployment
    -  ```
    -
    -  
    -  
    -
    -  ```bash
    -  oc --namespace  \
    -      wait \
    -      --for=condition=available \
    -      --timeout=20m \
    -      --selector app.kubernetes.io/instance= \
    -      deployment
    -  ```
    -
    -  
    -
    -
    -:::
    -
    -
    -#### 5. Access Rasa Open Source Assistant
    -
    -By default the Rasa deployment is exposed via the `rasa` (``) service and accessible only within a Kubernetes cluster. You can get
    -the IP address using this command:
    -
    -
    -  
    -
    -  ```bash
    -    export SERVICE_PORT=$(kubectl get --namespace  -o jsonpath="{.spec.ports[0].port}" services )
    -    kubectl port-forward --namespace  svc/ ${SERVICE_PORT}:${SERVICE_PORT} &
    -  ```
    -
    -  
    -  
    -
    -  ```bash
    -    export SERVICE_PORT=$(oc get --namespace  -o jsonpath="{.spec.ports[0].port}" services )
    -    oc port-forward --namespace  svc/ ${SERVICE_PORT}:${SERVICE_PORT} &
    -  ```
    -
    -  
    -
    -
    -You can then access the deployment on `http://127.0.0.1:${SERVICE_PORT}`
    -
    -Visit [the Rasa helm chart README](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa#exposing-the-rasa-deployment-to-the-public) to learn other ways to expose your deployment.
    -
    -#### Next Steps
    -
    -Visit [the Rasa helm chart repository](https://github.com/RasaHQ/helm-charts/tree/main/charts/rasa) where you can find examples of configuration and learn how to e.g. integrate your Rasa Open Source deployment with Rasa X.
    -
    -## Alternative Deployment Methods
    -
    -### Docker Compose
    -
    -You can also run Rasa X in a Docker Compose setup, without the cluster environment. We have an install script
    -for doing so, as well as manual instructions for any custom setups.
    -
    -* Default: Read the [Docker Compose Install Script](https://rasa.com/docs/rasa-x/installation-and-setup/install/docker-compose/#docker-compose-install-script) docs or watch the [Masterclass Video](https://www.youtube.com/watch?v=IUYdwy8HPVc) on deploying Rasa X.
    -
    -* Custom: Read the [Docker Compose Manual Install](https://rasa.com/docs/rasa-x/installation-and-setup/install/docker-compose/#docker-compose-manual-install) documentation for full customization options.
    -
    -### Rasa Open Source Only Deployment
    -
    -It is also possible to deploy a Rasa assistant without Rasa X using Docker Compose. To do so, you can build your
    -Rasa Assistant locally or in Docker. Then you can deploy your model in Docker Compose.
    -
    -* [Building a Rasa Assistant Locally](./playground.mdx)
    -
    -* [Building a Rasa Assistant in Docker](./docker/building-in-docker.mdx)
    -
    -* [Deploying a Rasa Open Source Assistant in Docker Compose](./docker/deploying-in-docker-compose.mdx)
    -
    -
    -## Deploying Your Action Server
    -
    -### Building an Action Server Image
    -
    -If you build an image that includes your action code and store it in a container registry, you can run it
    -as part of your deployment, without having to move code between servers.
    -In addition, you can add any additional dependencies of systems or Python libraries
    -that are part of your action code but not included in the base `rasa/rasa-sdk` image.
    -
    -#### Automating your Action Server Image Builds
    -
    -In addition to a manually creating a new Action Server image, you can use the [Rasa Action Server GitHub Action](https://github.com/RasaHQ/action-server-gha) to automate image builds.
    -If GitHub Actions are new to you, it might be helpful to get familiar with [GitHub Actions Documentation](https://docs.github.com/en/actions).
    -
    -The following steps assume that you already created a GitHub repository and you have a DockerHub account.
    -
    -To create a workflow for building and pushing a Docker image into a DockerHub registry:
    -
    -1. Add GitHub Secrets with your DockerHub login name and password.
    -   You can find details on how to create encrypted secrets for a repository
    -   in the [Github docs](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets-for-a-repository)
    -
    -   The example uses the following secrets:
    -   - `DOCKER_HUB_LOGIN` - a login name for DockerHub
    -   - `DOCKER_HUB_PASSWORD` - a password for DockerHub
    -
    -2. In your GitHub repository create a file [`.github/workflows/action_server.yml`](https://github.com/RasaHQ/action-server-gha/blob/master/examples/action_server.yml).
    -
    -The GitHub Action workflow below builds a new docker image every time files inside the `actions/` directory have changed and the changes are pushed into the `main` branch.
    -
    -```yaml
    -on:
    -  push:
    -    branches:
    -      - main
    -    paths:
    -    - 'actions/**'
    -
    -jobs:
    -  build_and_deploy:
    -    runs-on: ubuntu-latest
    -    name: Build Action Server image and upgrade Rasa X deployment
    -    steps:
    -    - name: Checkout repository
    -      uses: actions/checkout@v2
    -
    -    - id: action_server
    -      name: Build an action server with a custom actions
    -      uses: RasaHQ/action-server-gha@master
    -      # Full list of parameters: https://github.com/RasaHQ/action-server-gha/tree/master#input-arguments
    -      with:
    -        docker_image_name: 'account_username/repository_name'
    -        docker_registry_login: ${{ secrets.DOCKER_HUB_LOGIN }}
    -        docker_registry_password: ${{ secrets.DOCKER_HUB_PASSWORD }}
    -        # More details about github context:
    -        # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#github-context
    -        #
    -        # github.sha - The commit SHA that triggered the workflow run
    -        docker_image_tag: ${{ github.sha }}
    -```
    -
    -3. Push your changes to the `main` branch. After changes are pushed, the workflow will build and push a new image into the DockerHub registry.
    -
    -4. Now, you can use your new brand docker image.
    -
    -5. You can also extend your workflow, so that you do not have to manually update your Rasa X deployment. The example below shows how to extend your workflow with an additional step that updates a Rasa X [Helm Chart](https://rasa.com/docs/rasa-x/installation-and-setup/customize/#adding-a-custom-action-server) deployment.
    -
    -```yaml
    -on:
    -  push:
    -    branches:
    -      - main
    -
    -jobs:
    -  build_and_deploy:
    -    runs-on: ubuntu-latest
    -    name: Build Action Server image and upgrade Rasa X deployment
    -    steps:
    -    [..]
    -
    -    # This step shows only the example of output parameter usage
    -    # and it's not focused on deployment itself.
    -    - name: "Upgrade a Rasa X deployment"
    -      run: |
    -        helm upgrade --install --reuse-values \
    -          --set app.name=${{ steps.action_server.outputs.docker_image_name }} \
    -          --set app.tag=${{ steps.action_server.outputs.docker_image_tag }} rasa rasa-x/rasa-x
    -```
    -
    -As you can see it's possible to use output variables from the `action_server` step. The `steps.action_server.outputs.docker_image_name` variable returns
    -a docker image name and the `steps.action_server.outputs.docker_image_tag` variable returns a docker image tag.
    -
    -More examples on how to use and customize [Rasa GitHub Actions](https://github.com/RasaHQ/action-server-gha) you can find in the [Rasa GitHub Actions](https://github.com/RasaHQ/action-server-gha) repository.
    -
    -#### Manually Building an Action Server
    -
    -To create your image:
    -
    -1. Make sure your actions are defined in `actions/actions.py`. The `rasa/rasa-sdk`
    -  image will automatically look for the actions in this file.
    -
    -2. If your actions have any extra dependencies, create a list of them in a file,
    -   `actions/requirements-actions.txt`.
    -
    -3. Create a file named `Dockerfile` in your project directory,
    -   in which you'll extend the official SDK image, copy over your code, and add any custom dependencies (if necessary).
    -   For example:
    -
    -   
    
    -   {`# Extend the official Rasa SDK image
    -   FROM rasa/rasa-sdk:${variables.rasa_sdk_version}
    -
    -   # Use subdirectory as working directory
    -   WORKDIR /app
    -
    -   # Copy any additional custom requirements, if necessary (uncomment next line)
    -   # COPY actions/requirements-actions.txt ./
    -
    -   # Change back to root user to install dependencies
    -   USER root
    -
    -   # Install extra requirements for actions code, if necessary (uncomment next line)
    -   # RUN pip install -r requirements-actions.txt
    -
    -   # Copy actions folder to working directory
    -   COPY ./actions /app/actions
    -
    -   # By best practices, don't run the code with root user
    -   USER 1001`}
    - -You can then build the image via the following command: - -```bash -docker build . -t /: -``` - -The `` should reference how this image will be different from others. For -example, you could version or date your tags, as well as create different tags that have different code for production -and development servers. You should create a new tag any time you update your code and want to re-deploy it. - -### Using your Custom Action Server Image - -If you're building this image to make it available from another server, -for example a Rasa X or Rasa Enterprise deployment, you should push the image to a cloud repository. - -This documentation assumes you are pushing your images to [DockerHub](https://hub.docker.com/). -DockerHub will let you host multiple public repositories and -one private repository for free. Be sure to first [create an account](https://hub.docker.com/signup/) -and [create a repository](https://hub.docker.com/signup/) to store your images. You could also push images to -a different Docker registry, such as [Google Container Registry](https://cloud.google.com/container-registry), -[Amazon Elastic Container Registry](https://aws.amazon.com/ecr/), or -[Azure Container Registry](https://azure.microsoft.com/en-us/services/container-registry/). - -You can push the image to DockerHub via: - -```bash -docker login --username --password -docker push /: -``` - -To authenticate and push images to a different container registry, please refer to the documentation of -your chosen container registry. - -How you reference the custom action image will depend on your deployment. Pick the relevant documentation for -your deployment: - -* [Server Quick-Install](https://rasa.com/docs/rasa-x/installation-and-setup/customize/#quick-install-script-customizing) - -* [Helm Chart](https://rasa.com/docs/rasa-x/installation-and-setup/customize/#adding-a-custom-action-server) - -* [Docker Compose](https://rasa.com/docs/rasa-x/installation-and-setup/customize/#connecting-a-custom-action-server) - -* [Rasa Open Source Only](./docker/deploying-in-docker-compose.mdx#using-docker-compose-to-run-multiple-services) diff --git a/docs/docs/setting-up-ci-cd.mdx b/docs/docs/setting-up-ci-cd.mdx index 5b7804425242..8f8e9cffdf94 100644 --- a/docs/docs/setting-up-ci-cd.mdx +++ b/docs/docs/setting-up-ci-cd.mdx @@ -135,7 +135,7 @@ actions that don't exist in the pre-update action server. ### Deploying Your Action Server You can automate -[building and uploading a new image for your action server](./how-to-deploy.mdx#building-an-action-server-image) +[building and uploading a new image for your action server](https://rasa.com/docs/action-server/deploy-action-server#building-an-action-server-image) to an image repository for each update to your action code. As noted above, be careful with automatically deploying a new image tag to production if the action server diff --git a/docs/docs/tracker-stores.mdx b/docs/docs/tracker-stores.mdx index 563aba2084ee..7e43af4b751a 100644 --- a/docs/docs/tracker-stores.mdx +++ b/docs/docs/tracker-stores.mdx @@ -155,7 +155,7 @@ Then build the docker image: Now you can configure the tracker store in the `endpoints.yml` as described above, and start the container. The `dialect` parameter with this setup will be `oracle+cx_oracle`. -Read more about [Deploying Your Rasa Assistant](./how-to-deploy.mdx). +Read more about [Deploying a Rasa Assistant](./deploy/introduction.mdx). ## RedisTrackerStore diff --git a/docs/sidebars.js b/docs/sidebars.js index 96b628b0e29c..da69374779af 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -38,12 +38,31 @@ module.exports = { 'tuning-your-model', 'testing-your-assistant', 'setting-up-ci-cd', - 'how-to-deploy', ], }, "glossary", ], }, + { + type: 'category', + label: 'Deploying Assistants', + collapsed: true, + items: [ + 'deploy/introduction', + 'deploy/deploy-rasa', + 'deploy/deploy-action-server', + 'deploy/deploy-rasa-x', + { + type: 'category', + label: 'Deployment Tools', + collapsed: true, + items: [ + 'deploy/rei/using-rei' + + ], + } + ], + }, { type: 'category', label: 'Concepts', From e3d62e677a8ed0dc13c039cafcbac909a3b4f4c7 Mon Sep 17 00:00:00 2001 From: emysdias Date: Tue, 8 Mar 2022 00:25:23 -0300 Subject: [PATCH 65/65] Add check loop def Co-authored-by: WashingtonBispo --- changelog/10925.improvement.md | 1 + rasa/validator.py | 56 +++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 changelog/10925.improvement.md diff --git a/changelog/10925.improvement.md b/changelog/10925.improvement.md new file mode 100644 index 000000000000..65116591313e --- /dev/null +++ b/changelog/10925.improvement.md @@ -0,0 +1 @@ +Add verify loop caused by checkpoints in "rasa data validate" \ No newline at end of file diff --git a/rasa/validator.py b/rasa/validator.py index 36d160d651e3..8f3a1ef6c19f 100644 --- a/rasa/validator.py +++ b/rasa/validator.py @@ -1,8 +1,13 @@ import itertools import logging +import queue from collections import defaultdict +from platform import node +from re import A from typing import Set, Text, Optional, Dict, Any, List +from numpy import False_ + import rasa.core.training.story_conflict import rasa.shared.nlu.constants from rasa.shared.constants import ( @@ -24,6 +29,10 @@ from rasa.shared.nlu.training_data.training_data import TrainingData import rasa.shared.utils.io +from rasa.shared.core.training_data.structures import ( + STORY_START, +) + logger = logging.getLogger(__name__) @@ -91,6 +100,47 @@ def verify_intents(self, ignore_warnings: bool = True) -> bool: everything_is_alright = False return everything_is_alright + + def verify_loop_in_intents( + self, ignore_warnings: bool = True + ) -> bool: + row = queue.Queue() + nodes = dict() + visited = dict() + loops_cp = [] + + everything_is_alright = True + + for story in self.story_graph.story_steps: + start_cp = story.start_checkpoints[0].name + if start_cp not in nodes: + nodes[start_cp] = [] + visited[start_cp] = False + if len(story.end_checkpoints) > 0: + end_cp = story.end_checkpoints[0].name + if(end_cp not in nodes): + nodes[end_cp] = [] + visited[end_cp] = False + nodes[start_cp].append(end_cp) + + if STORY_START in nodes: + row.put(STORY_START) + while not row.empty(): + x = row.get() + visited[x] = True + for node in nodes[x]: + if visited[node]: + loops_cp.append(f"{x} => {node}") + everything_is_alright = ignore_warnings and everything_is_alright + else: + row.put(node) + + if(len(loops_cp) > 0): + rasa.shared.utils.io.raise_warning( + f"These checkpoints '{loops_cp}' is causing loop" + ) + + return everything_is_alright def verify_example_repetition_in_intents( self, ignore_warnings: bool = True @@ -327,10 +377,14 @@ def verify_nlu(self, ignore_warnings: bool = True) -> bool: there_is_no_duplication = self.verify_example_repetition_in_intents( ignore_warnings ) + + logger.info("Validating loop of checkpoints...") + loop_in_checkpoint = self.verify_loop_in_intents(ignore_warnings) logger.info("Validating utterances...") stories_are_valid = self.verify_utterances_in_stories(ignore_warnings) - return intents_are_valid and stories_are_valid and there_is_no_duplication + return (intents_are_valid and stories_are_valid and there_is_no_duplication + and loop_in_checkpoint) def verify_form_slots(self) -> bool: """Verifies that form slots match the slot mappings in domain."""