From 3e6fc8339130086dad4a748f03fda61ced9a817a Mon Sep 17 00:00:00 2001 From: Xurshid Muhammadiyev Date: Wed, 22 Jul 2026 15:44:18 +0500 Subject: [PATCH] Publish Russian Rasa intent assistant --- .github/workflows/ci.yml | 63 ++ .gitignore | 55 ++ .python-version | 1 + README.md | 212 +++++ actions/__init__.py | 1 + actions/actions.py | 104 +++ config.yml | 34 + data/nlu.yml | 860 ++++++++++++++++++ data/rules.yml | 113 +++ data/stories.yml | 93 ++ docker-compose.yml | 44 + domain.yml | 104 +++ endpoints.docker.yml | 2 + endpoints.yml | 2 + requirements-dev.txt | 2 + requirements.txt | 2 + results/README.md | 54 ++ results/experiments.md | 62 ++ .../final/DIETClassifier_confusion_matrix.png | Bin 0 -> 37225 bytes results/final/DIETClassifier_errors.json | 64 ++ results/final/DIETClassifier_histogram.png | Bin 0 -> 40958 bytes results/final/DIETClassifier_report.json | 28 + results/final/intent_confusion_matrix.png | Bin 0 -> 101442 bytes results/final/intent_errors.json | 202 ++++ results/final/intent_histogram.png | Bin 0 -> 42179 bytes results/final/intent_report.json | 194 ++++ results/final/runtime_report.json | 111 +++ results/final/story_report.json | 171 ++++ results/final/summary.md | 82 ++ scripts/chat.ps1 | 105 +++ scripts/evaluate.ps1 | 174 ++++ scripts/evaluate_runtime_nlu.py | 374 ++++++++ scripts/summarize_results.py | 255 ++++++ scripts/train.ps1 | 61 ++ scripts/validate.ps1 | 53 ++ tests/nlu_dev.yml | 302 ++++++ tests/nlu_test.yml | 302 ++++++ tests/static_validate.py | 806 ++++++++++++++++ tests/test_actions.py | 140 +++ tests/test_stories.yml | 83 ++ 40 files changed, 5315 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 actions/__init__.py create mode 100644 actions/actions.py create mode 100644 config.yml create mode 100644 data/nlu.yml create mode 100644 data/rules.yml create mode 100644 data/stories.yml create mode 100644 docker-compose.yml create mode 100644 domain.yml create mode 100644 endpoints.docker.yml create mode 100644 endpoints.yml create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 results/README.md create mode 100644 results/experiments.md create mode 100644 results/final/DIETClassifier_confusion_matrix.png create mode 100644 results/final/DIETClassifier_errors.json create mode 100644 results/final/DIETClassifier_histogram.png create mode 100644 results/final/DIETClassifier_report.json create mode 100644 results/final/intent_confusion_matrix.png create mode 100644 results/final/intent_errors.json create mode 100644 results/final/intent_histogram.png create mode 100644 results/final/intent_report.json create mode 100644 results/final/runtime_report.json create mode 100644 results/final/story_report.json create mode 100644 results/final/summary.md create mode 100644 scripts/chat.ps1 create mode 100644 scripts/evaluate.ps1 create mode 100644 scripts/evaluate_runtime_nlu.py create mode 100644 scripts/summarize_results.py create mode 100644 scripts/train.ps1 create mode 100644 scripts/validate.ps1 create mode 100644 tests/nlu_dev.yml create mode 100644 tests/nlu_test.yml create mode 100644 tests/static_validate.py create mode 100644 tests/test_actions.py create mode 100644 tests/test_stories.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a17fb1b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Schema, data, and unit checks (no training) + runs-on: ubuntu-22.04 + timeout-minutes: 20 + env: + PYTHONUTF8: "1" + PYTHONIOENCODING: "utf-8" + RASA_TELEMETRY_ENABLED: "false" + TF_CPP_MIN_LOG_LEVEL: "2" + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10.11" + cache: pip + cache-dependency-path: | + requirements.txt + requirements-dev.txt + + - name: Install pinned dependencies + run: | + python -m pip install pip==24.3.1 + python -m pip install -r requirements-dev.txt + python -m pip check + python -m rasa --version + + - name: Validate Rasa data and domain + run: >- + python -m rasa data validate + --domain domain.yml + --data data + --fail-on-warnings + + - name: Run project-level static validation + run: python tests/static_validate.py + + - name: Run Python unit tests when present + shell: bash + run: | + if find tests -type f \( -name 'test_*.py' -o -name '*_test.py' \) -print -quit | grep -q .; then + python -m pytest -q + else + echo "No Python unit tests found; pytest skipped." + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4aa8203 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Python +__pycache__/ +*.py[cod] +.pytest_cache/ +.coverage +htmlcov/ +venv/ +.venv/ +.venv-rasa/ +.runtime/ + +# Rasa and generated evaluation data +.rasa/ +models/ +results/* +!results/README.md +!results/experiments.md +!results/final/ +results/final/* +!results/final/summary.md +!results/final/*_report.json +!results/final/*_errors.json +!results/final/*_confusion_matrix.png +!results/final/*_histogram.png +!results/final/runtime_report.json + +# Local application state +.config/ +.keras/ +.env +.env.* +!.env.example +*.log +.agents/ +.codex/ + +# IDE and operating system +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# Historical unpacked duplicate and local audit artifacts +/rasa_diploma/ +/artifacts/ +/outputs/ +/output/ +/document_tools/ +/.audit_docx/ +*.backup + +# Personal diploma artifacts are not part of the public code repository +*.docx +*.pptx +*.pdf diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..09dcc78 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.11 diff --git a/README.md b/README.md new file mode 100644 index 0000000..24d1d41 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# Русскоязычный intent-based ассистент на Rasa + +Восстановленный и заново обученный дипломный проект Хуршида Мухаммадиева. +Это классическая диалоговая система: она не генерирует текст через LLM и не +обращается к внешнему API, а обучается на собственном размеченном корпусе, +распознаёт намерение пользователя и выбирает подготовленный ответ или действие. + +Проект специально оставлен CLI-first. Веб-интерфейс и публичный REST API здесь +не нужны: основная работа — данные, ML/NLU, оценка качества и воспроизводимый +запуск. + +## Результат + +Финальная модель обучена на 795 русскоязычных примерах и оценена на 235 +отложенных test-сообщениях. Test split не участвовал в обучении; после выбранного +финального прогона train и конфигурация модели не изменялись. + +| Метрика | Результат | +|---|---:| +| Raw Intent Accuracy | **89,36%** | +| Intent Macro-F1 | **90,23%** | +| Intent Weighted-F1 | **89,26%** | +| `person_name` token/tag F1 | **87,80%** | +| Строгая accuracy после fallback | **88,94%** | +| Coverage без fallback | **97,02%** | +| Selective accuracy | **91,67%** | + +Полный отчёт: [results/final/summary.md](results/final/summary.md). История двух +итераций и прирост от улучшения данных: [results/experiments.md](results/experiments.md). + +![Матрица ошибок intent](results/final/intent_confusion_matrix.png) + +Все цифры получены реальным запуском закреплённого окружения. Корпус составлен +вручную/синтетически, поэтому эти метрики не выдаются за качество на сообщениях +реальных пользователей. + +## Что обучается + +- 21 пользовательский intent: приветствие, помощь, вопросы о проекте, Rasa, + NLU, обучении, запуске, Docker, времени и другие сценарии; +- `DIETClassifier` классифицирует intent и извлекает сущность `person_name`; +- slot `user_name` хранит найденное имя в пределах текущей сессии; +- `FallbackClassifier` безопасно отклоняет запросы с низкой уверенностью; +- `RulePolicy` и `TEDPolicy` выбирают следующий ответ или custom action; +- четыре Python-action возвращают возможности, запоминают имя и называют дату + или время Ташкента. + +Примеры маршрутизации: + +```text +«Кто разработал помощника?» + → ask_creator → utter_creator + +«Меня зовут Азамат» + → inform_name + person_name="Азамат" → action_remember_name + +неподдерживаемая тема + → out_of_scope или nlu_fallback → ограниченный безопасный ответ +``` + +## Архитектура + +```mermaid +flowchart LR + U["Русская реплика"] --> F["Word и char_wb n-grams"] + F --> D["DIETClassifier"] + D --> N["intent + person_name"] + D -->|"низкая уверенность"| B["FallbackClassifier"] + N --> C["RulePolicy / TEDPolicy"] + B --> C + C --> R["Готовый ответ"] + C --> A["Python custom action"] +``` + +Стек закреплён для повторяемого запуска: Python 3.10.11, Rasa 3.6.21 и Rasa SDK +3.6.2. У DIET/TED по 100 эпох и `random_seed: 42`. Прямые зависимости закреплены, +но полного lock-файла транзитивных пакетов нет, поэтому побитовая идентичность +переобученной модели не обещается. + +## Данные и честная оценка + +| Split | Файл | Примеров | Назначение | +|---|---|---:|---| +| train | `data/nlu.yml` | 795 | обучение модели | +| dev | `tests/nlu_dev.yml` | 235 | диагностика и улучшение границ intent | +| test | `tests/nlu_test.yml` | 235 | финальная отложенная оценка | + +`tests/static_validate.py` проверяет таксономию, размеры split, YAML-связи, +реализации actions, закреплённые Docker-образы и гигиену разделения данных. Он +автоматически подтверждает: + +- отсутствие точных пересечений train/dev/test; +- отсутствие совпадающих train/test-шаблонов после маскировки entity; +- новые значения `person_name` в dev и test; +- по выбранной текстовой эвристике нет пар одного intent между train/test и + dev/test со score `>= 0,85`. + +Последняя проверка чувствительна к выбранной метрике и порогу: она не исключает +семантически близкие фразы и не доказывает статистическую независимость или +качество на реальном пользовательском трафике. + +`rasa test nlu` показывает исходное качество классификатора без влияния +fallback. `scripts/evaluate_runtime_nlu.py` отдельно пропускает те же сообщения +через полный pipeline и считает coverage, fallback rate и selective accuracy, +не сохраняя тексты сообщений в runtime-отчёт. + +В финальном прогоне fallback отклонил семь сообщений: шесть с ошибочным raw +intent и одно с верным. Поэтому он повысил точность среди принятых intent ценой +3% coverage и снижения строгой общей accuracy на 0,43 процентного пункта. + +## Быстрый запуск на Windows + +Нужен именно Python 3.10: текущая ветка Rasa 3.6 несовместима с Python 3.11+. + +```powershell +py -3.10 -m venv .venv-rasa +.\.venv-rasa\Scripts\python.exe -m pip install -r requirements-dev.txt +``` + +Проверка, обучение и финальная оценка: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\validate.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\train.ps1 -Force +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\evaluate.ps1 +``` + +Оценка выполняет три прохода: штатный `rasa test nlu`, полный NLU pipeline с +`FallbackClassifier` и Core-тест диалоговой политики на той же модели. Модели +сохраняются в `models/`, а отчёты — в `results/final/`. + +Интерактивный разговор: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\chat.ps1 +``` + +Скрипт скрыто запускает локальный action server, ждёт его healthcheck, открывает +`rasa shell` и завершает фоновый процесс при выходе. + +## Запуск через Docker + +Docker не публикует HTTP-порты: контейнеры используются только для +воспроизводимого CLI-запуска. + +```powershell +docker compose run --rm --no-deps rasa data validate --fail-on-warnings +docker compose run --rm --no-deps rasa train +docker compose run --rm rasa +docker compose down +``` + +Rasa и action server общаются только во внутренней сети Compose через +`endpoints.docker.yml`. + +## Проверки + +```powershell +# статические инварианты и штатная валидация Rasa +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\validate.ps1 + +# unit-тесты четырёх custom actions +.\.venv-rasa\Scripts\python.exe -m pytest -q + +# Core-сценарии для уже обученной модели +.\.venv-rasa\Scripts\python.exe -m rasa test core ` + --model .\models\.tar.gz ` + --stories .\tests\test_stories.yml ` + --out .\results\core +``` + +Проверенный результат Core: **10/10 историй и 47/47 действий**, из которых 23 — +технические `action_listen`. Это Core-only проверка с заранее заданными gold +intents, а не end-to-end оценка распознавания пользовательских фраз. GitHub +Actions выполняет schema/data/unit checks, но намеренно не обучает DIET/TED на +каждом push; зелёный CI сам по себе не подтверждает ML-метрики. + +## Структура репозитория + +```text +actions/ custom actions на Python +data/ train NLU, rules и stories +scripts/ validate/train/evaluate/chat +tests/ dev/test NLU, Core и unit-тесты +results/final/ реальные отчёты выбранной модели +config.yml NLU pipeline и dialogue policies +domain.yml intents, entity, slot, ответы и actions +docker-compose.yml CLI-only контейнерный запуск +requirements*.txt закреплённые зависимости +``` + +Модели, виртуальные окружения, личные DOCX/PPTX/PDF, логи и старые копии проекта +исключены из Git. Для публичного репозитория подготовлены только код, данные и +проверяемые результаты экспериментов. + +## Ограничения + +- test-данные не являются выборкой из реального пользовательского трафика; +- модель выбирает подготовленные ответы и не решает произвольные задачи; +- Rasa Open Source 3.6 — legacy-ветка: её период поддержки уже завершён согласно + [официальной политике Rasa](https://rasa.com/rasa-product-release-and-maintenance-policy). + Она оставлена для исторической совместимости учебного проекта, но не + рекомендуется как основа нового production-сервиса; +- Docker-конфигурация подготовлена, но локальный прогон в этой среде не + выполнялся из-за отсутствия Docker. + +Следующий содержательный шаг для ML-части — собрать анонимный набор реальных +реплик, не изменять по нему train и повторить независимую оценку. + +## Лицензия + +Код и данные проекта распространяются по лицензии [MIT](LICENSE). diff --git a/actions/__init__.py b/actions/__init__.py new file mode 100644 index 0000000..1298755 --- /dev/null +++ b/actions/__init__.py @@ -0,0 +1 @@ +"""Custom actions package for the Rasa SDK action server.""" diff --git a/actions/actions.py b/actions/actions.py new file mode 100644 index 0000000..c021dc0 --- /dev/null +++ b/actions/actions.py @@ -0,0 +1,104 @@ +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, List, Text + +from rasa_sdk import Action, Tracker +from rasa_sdk.executor import CollectingDispatcher + + +class ActionRememberName(Action): + def name(self) -> Text: + return "action_remember_name" + + def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + user_name = tracker.get_slot("user_name") + + if user_name: + dispatcher.utter_message( + text=f"Приятно познакомиться, {user_name}! Я запомню ваше имя в рамках текущего диалога." + ) + else: + dispatcher.utter_message( + text="Я не смог выделить имя. Напишите, например: «Меня зовут Анна»." + ) + + return [] + + +class ActionShowCapabilities(Action): + def name(self) -> Text: + return "action_show_capabilities" + + def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + user_name = tracker.get_slot("user_name") + subject = f"{user_name}, я" if user_name else "Я" + dispatcher.utter_message( + text=( + f"{subject} могу рассказать о себе, авторе и назначении проекта, " + "объяснить Rasa, NLU, Docker, обучение, запуск и структуру файлов, " + "запомнить имя, назвать дату и время, поддержать короткий разговор " + "и пошутить." + ) + ) + return [] + + +class ActionShowTime(Action): + def name(self) -> Text: + return "action_show_time" + + def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + tashkent_time = datetime.now(timezone(timedelta(hours=5))) + dispatcher.utter_message( + text=f"Сейчас {tashkent_time:%H:%M} по времени Ташкента (UTC+5)." + ) + return [] + + +class ActionShowDate(Action): + def name(self) -> Text: + return "action_show_date" + + def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + tashkent_date = datetime.now(timezone(timedelta(hours=5))) + month_names = ( + "января", + "февраля", + "марта", + "апреля", + "мая", + "июня", + "июля", + "августа", + "сентября", + "октября", + "ноября", + "декабря", + ) + dispatcher.utter_message( + text=( + f"Сегодня {tashkent_date.day} " + f"{month_names[tashkent_date.month - 1]} {tashkent_date.year} года " + "по времени Ташкента." + ) + ) + return [] diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..b8a7b78 --- /dev/null +++ b/config.yml @@ -0,0 +1,34 @@ +recipe: default.v1 +language: ru + +pipeline: + - name: WhitespaceTokenizer + - name: LexicalSyntacticFeaturizer + - name: CountVectorsFeaturizer + analyzer: word + - name: CountVectorsFeaturizer + analyzer: char_wb + min_ngram: 3 + max_ngram: 5 + - name: DIETClassifier + epochs: 100 + constrain_similarities: true + random_seed: 42 + - name: FallbackClassifier + threshold: 0.55 + ambiguity_threshold: 0.10 + +policies: + - name: MemoizationPolicy + max_history: 5 + - name: RulePolicy + core_fallback_threshold: 0.30 + core_fallback_action_name: utter_default + enable_fallback_prediction: true + - name: TEDPolicy + max_history: 5 + epochs: 100 + constrain_similarities: true + random_seed: 42 + +assistant_id: diploma-russian-study-assistant diff --git a/data/nlu.yml b/data/nlu.yml new file mode 100644 index 0000000..521eece --- /dev/null +++ b/data/nlu.yml @@ -0,0 +1,860 @@ +version: "3.1" + +nlu: +- intent: greet + examples: | + - привет + - здравствуй + - здравствуйте + - доброе утро + - добрый день + - добрый вечер + - приветствую + - салют + - рад встрече + - рад тебя видеть + - начинаем разговор + - давай познакомимся + - всем привет + - привет, помощник + - здравствуй, бот + - доброго утра + - доброго дня + - доброго вечера + - я снова здесь + - можно начать беседу + - хочу поздороваться + - передаю привет + - привет тебе + - ну здравствуй + - хей, начнём + - предлагаю познакомиться и начать разговор + - помощник, выходи на связь + - открываем новую беседу + - рад нашей первой встрече + - в диалог вошёл новый пользователь + - можем начинать общение + - приветствую виртуального собеседника + - я пришёл поговорить с помощником + - давай откроем разговор + - начинаю общение с ботом + +- intent: goodbye + examples: | + - пока + - до свидания + - до встречи + - всего доброго + - всего хорошего + - увидимся позже + - мне пора + - я ухожу + - завершим беседу + - закончим разговор + - на этом всё + - буду прощаться + - хорошего дня + - спокойной ночи + - ещё увидимся + - до скорого + - прощай + - отключаюсь + - мне нужно идти + - спасибо, пока + - до следующего раза + - заверши диалог + - больше вопросов нет + - увидимся + - заканчиваем + - пора закрывать наш разговор + - вернусь с вопросами в другой раз + - я закончил работу с помощником + - остановим беседу на сегодня + - благодарю, теперь буду прощаться + - закрой текущий диалог + - продолжим общение как-нибудь потом + - на сегодня разговор окончен + - мне пора отключиться от чата + - желаю успехов и ухожу + +- intent: thanks + examples: | + - спасибо + - большое спасибо + - благодарю + - спасибо за помощь + - очень признателен + - премного благодарен + - ты мне помог + - благодарю за объяснение + - спасибо за подробности + - это было полезно, спасибо + - ценю твою помощь + - спасибо за понятный ответ + - огромное спасибо + - благодарю тебя + - признателен за информацию + - выручил, спасибо + - от души благодарю + - спасибо, теперь понятно + - благодарю за подсказку + - весьма признателен + - спасибо за разъяснение + - ты очень помог, благодарю + - выражаю благодарность + - спасибо за уделённое время + - благодарю за точный ответ + - твоя рекомендация мне пригодилась + - подсказка оказалась полезной + - теперь всё стало ясно + - хорошо объяснил, благодарю + - ответ действительно помог разобраться + - ценная информация, признателен + - благодаря тебе я понял тему + - отличный совет, он сработал + - это именно то, что было нужно + - помощь была кстати + +- intent: help + examples: | + - что ты умеешь + - чем ты можешь помочь + - покажи свои возможности + - какие у тебя функции + - о чём тебя можно спросить + - перечисли доступные команды + - расскажи, что тебе известно + - нужна подсказка по возможностям + - с какими вопросами ты работаешь + - объясни, чем занимаешься + - дай список доступных тем + - в чём ты можешь быть полезен + - подскажи, как с тобой общаться + - какие запросы ты понимаешь + - покажи справку + - мне нужна помощь + - помоги разобраться с твоими функциями + - что можно здесь узнать + - какие темы поддерживаются + - расскажи о доступных действиях + - что мне у тебя спросить + - как пользоваться этим помощником + - открой перечень возможностей + - нужна инструкция по общению + - на какие вопросы ты отвечаешь + - сделай обзор доступных навыков + - покажи пользовательское руководство + - объясни правила разговора с тобой + - перечисли темы, в которых ты ориентируешься + - с чего начать работу с помощником + - предложи несколько вариантов вопроса + - дай короткую памятку пользователю + - какие действия здесь доступны + - познакомь меня со своими функциями + - подскажи поддерживаемые направления + +- intent: ask_name + examples: | + - кто ты + - как тебя зовут + - представься + - у тебя есть имя + - как к тебе обращаться + - назови своё имя + - ты кто такой + - какое у тебя имя + - скажи, кто ты + - кем ты являешься + - как называется этот помощник + - можешь представиться + - расскажи о себе кратко + - что ты за ассистент + - ты бот или человек + - какое имя у бота + - под каким именем ты работаешь + - хочу узнать твоё имя + - кто сейчас мне отвечает + - как мне тебя называть + - обозначь себя + - что ты собой представляешь + - назови себя + - с кем я разговариваю + - кто мой собеседник + - сообщи имя виртуального ассистента + - под каким названием тебя знают + - как подписан этот бот + - представь личность собеседника + - какое обращение выбрать для тебя + - назови себя как участника диалога + - хочу понять, кто отвечает мне сейчас + - какое имя носит помощник + - идентифицируй себя как ассистента + - с каким ботом я веду разговор + +- intent: ask_creator + examples: | + - кто тебя создал + - кто твой автор + - кем ты разработан + - кто написал этого бота + - расскажи о создателе + - кто разработчик ассистента + - назови автора программы + - чей это проект + - кто занимался разработкой + - кто сделал эту систему + - кому принадлежит авторство + - расскажи о своём разработчике + - какой студент тебя реализовал + - кто подготовил дипломного бота + - назови имя создателя + - кем создан этот помощник + - кто является автором работы + - чья разработка передо мной + - кто программировал ассистента + - сообщи данные разработчика + - кто работал над этим ботом + - автор проекта кто + - кто реализовал данную систему + - чьим дипломным проектом ты являешься + - расскажи, кем была выполнена работа + - кто выступил исполнителем дипломной разработки + - назови студента-разработчика системы + - кто написал исходный код помощника + - чья работа представлена в репозитории + - сообщи имя автора реализации + - какой разработчик собрал этого ассистента + - кто отвечает за создание программы + - кем выполнена программная часть диплома + - укажи человека, разработавшего бота + - кому принадлежит реализация проекта + +- intent: ask_project + examples: | + - расскажи о дипломном проекте + - какая тема проекта + - какова цель этой работы + - что демонстрирует проект + - для чего создан этот ассистент + - о чём твой проект + - в чём состоит дипломная разработка + - какую задачу решает система + - опиши назначение проекта + - какова идея диплома + - что было разработано в рамках работы + - чему посвящено исследование + - какой результат получен в проекте + - расскажи содержание дипломной работы + - зачем создавался виртуальный помощник + - в чём практический смысл разработки + - какая проблема рассматривается в дипломе + - опиши проект одним абзацем + - что является объектом разработки + - какую концепцию показывает ассистент + - сформулируй цель диплома + - что представляет собой эта работа + - какие задачи ставились в проекте + - расскажи о тематике исследования + - для какой демонстрации сделан бот + - каков основной замысел разработки + - дай общее описание созданной системы + - какую пользу демонстрирует этот проект + - в чём заключается главная цель решения + - резюмируй содержание разработки + - какой результат подтверждает эта разработка + - объясни идею виртуального помощника + - что исследуется с помощью этого бота + - назови назначение готовой системы + - какой практический результат у диплома + +- intent: inform_name + examples: | + - меня зовут [Александр](person_name) + - моё имя [Екатерина](person_name) + - я [Михаил](person_name) + - можешь звать меня [Наталья](person_name) + - обращайся ко мне как [Тимур](person_name) + - хочу представиться: [Диана](person_name) + - моё полное имя [Иван Петров](person_name) + - я представлюсь, [Анна Смирнова](person_name) + - называй меня [Али-Шер](person_name) + - для тебя я [Мария-Луиза](person_name) + - запомни имя [Абдулла Рахимов](person_name) + - при знакомстве меня называют [Софья](person_name) + - в этом диалоге я [Рустам](person_name) + - моё имя и отчество [Елена Викторовна](person_name) + - записывай: [Павел Сергеевич](person_name) + - представлюсь как [Николай](person_name) + - друзья зовут меня [Зарина](person_name) + - можно обращаться [Олег Ким](person_name) + - в беседе используй имя [Светлана Орлова](person_name) + - представь, что меня зовут [Бекзод Юлдашев](person_name) + - буду знакома, [Виктория](person_name) + - приятно познакомиться, я [Артём](person_name) + - при обращении говори [Камилла](person_name) + - сохрани, пожалуйста, имя [Фаррух](person_name) + - здесь я записан как [Лев](person_name) + - представь меня именем [Надежда](person_name) + - зовут меня [Илья Воронцов](person_name) + - в профиле указано [Малика Абдуллаева](person_name) + - мое имя пишется [Жан-Клод](person_name) + - ко мне можно обращаться по имени [Алина](person_name) + - скажу своё имя — [Георгий](person_name) + - разрешаю называть меня [Лилия](person_name) + - пожалуйста, запомни: [Владислав](person_name) + - собеседника зовут [Инна](person_name) + - я хотел бы представиться как [Денис](person_name) + - используйте для обращения имя [Эльвира](person_name) + - во время разговора называй меня [Константин](person_name) + - моё настоящее имя — [Айгуль](person_name) + - давай знакомиться, я [Роман](person_name) + - можешь сохранить имя [Татьяна](person_name) + - укажи в профиле имя [Ярослав](person_name) + - для текущего разговора я [Саида](person_name) + - обращайся к собеседнику [Кирилл Новиков](person_name) + - мой профиль принадлежит [Азиза Ахмедова](person_name) + - можешь говорить мне [Арсен](person_name) + - запиши моё имя: [Мирослава](person_name) + - в этом чате называй меня [Даврон](person_name) + - полное имя пользователя [Полина Громова](person_name) + - договоримся, что я [Шахзода](person_name) + - обращение для меня — [Евгений Ильин](person_name) + - создай запись пользователя [Злата](person_name) + - в текущем чате используй [Умар Саидов](person_name) + - моим именем будет [Вероника](person_name) + - добавь в карточку [Борис Кузнецов](person_name) + - прошу обращаться ко мне [Нигина](person_name) + - имя участника диалога — [Самир](person_name) + - сохрани правильное написание: [Яна Белова](person_name) + - можешь считать, что я [Антон](person_name) + - полностью меня зовут [Мадина Юсупова](person_name) + - выбери для меня обращение [Глеб](person_name) + +- intent: ask_restart + examples: | + - начни диалог заново + - давай сначала + - перезапусти разговор + - сбрось диалог + - хочу начать сначала + - очисти нашу беседу + - забудь текущий контекст + - верни разговор к началу + - перезагрузи сессию + - сотри историю этого диалога + - начнём общение с чистого листа + - сбрось сохранённые данные беседы + - хочу новую сессию + - аннулируй текущий разговор + - выполни перезапуск чата + - удали контекст и начни снова + - обнули состояние помощника + - перезапусти текущую беседу + - верни исходное состояние + - начни новый сеанс общения + - забудь всё из этого разговора + - очисти память текущей сессии + - можно открыть новый диалог + - прекрати эту сессию и создай новую + - хочу поговорить заново без истории + - удали предыдущие сообщения из контекста + - создай пустую беседу с самого начала + - верни диалог к пустому начальному состоянию + - очисти всё, что бот помнит о разговоре + - открой свежую сессию без прошлого + - начни новый чат с нулевым контекстом + - сбрось память и вернись к старту + - отмени историю текущего общения + - перезапусти помощника для новой беседы + - хочу чистый диалог без прежних данных + +- intent: ask_rasa + examples: | + - что такое Rasa + - расскажи о платформе Rasa + - для чего предназначена Rasa + - какую роль Rasa играет в проекте + - почему выбрана именно Rasa + - Rasa является фреймворком + - опиши основные компоненты Rasa + - что умеет платформа Rasa + - чем Rasa полезна для чат-ботов + - дай определение Rasa + - какая архитектура у Rasa + - из каких частей состоит Rasa + - чем Rasa отличается от обычного скрипта + - является ли Rasa открытым решением + - объясни назначение Rasa в ассистенте + - на чём основана платформа Rasa + - какие задачи решает Rasa как система + - что входит в экосистему Rasa + - расскажи об устройстве Rasa в общих чертах + - зачем чат-боту нужен фреймворк Rasa + - почему Rasa подходит для диалоговой системы + - какую часть помощника обеспечивает Rasa + - что представляет собой Rasa Open Source + - где в архитектуре находится Rasa + - объясни идею использования Rasa + - перечисли основные возможности фреймворка Rasa + - каково назначение платформы для диалогов + - какую функцию Rasa выполняет в ассистенте + - дай обзор Rasa без описания тренировки + - почему этот фреймворк подходит для NLU-бота + - какие подсистемы объединяет платформа + - опиши Rasa как инструмент разработчика + - что предоставляет открытая версия Rasa + - для чего в решении используется этот фреймворк + - расскажи о роли Rasa в общем виде + +- intent: ask_nlu + examples: | + - что означает NLU + - как ассистент понимает текст + - что такое распознавание намерений + - как бот определяет intent + - какую информацию содержит entity + - объясни обработку естественного языка + - что называется намерением пользователя + - каким образом определяется смысл сообщения + - зачем извлекать сущности из фразы + - в чём задача NLU-компонента + - как текст превращается в структурированные данные + - чем intent отличается от entity + - что происходит с сообщением на этапе NLU + - как классифицируются пользовательские запросы + - объясни термин классификация намерений + - для чего нужна разметка сущностей + - как система выделяет имя в сообщении + - какие данные выдаёт NLU после анализа + - что понимается под естественным языком + - как определяется категория вопроса + - зачем диалоговой системе анализ текста + - расскажи о задаче intent classification + - каким результатом завершается NLU-анализ + - что модель ищет в пользовательской фразе + - как связаны сообщение, intent и entity + - зачем аннотировать имя внутри примера + - как модель выбирает категорию реплики + - объясни смысл сущности person_name + - каким образом фраза относится к одному intent + - что извлекается из текста при понимании запроса + - почему intent описывает цель пользователя + - как DIET распознаёт смысл входного сообщения + - зачем разделять намерения и сущности + - какой структурированный результат создаёт NLU + - как имя обнаруживается в новой реплике + +- intent: ask_training + examples: | + - как обучается модель + - что требуется для обучения классификатора + - каким образом создают обученную модель + - расскажи об этапе тренировки + - на каких данных учится ассистент + - что происходит во время машинного обучения + - как переобучить NLU после изменения примеров + - сколько эпох используется при тренировке + - какие файлы участвуют в обучении модели + - как запустить процесс тренировки + - для чего модели нужны размеченные фразы + - что получает алгоритм на вход при обучении + - как формируется архив обученной модели + - когда нужно выполнять повторное обучение + - объясни последовательность обучения + - какие компоненты настраиваются во время тренировки + - как данные влияют на качество классификатора + - где задаётся количество эпох обучения + - можно ли обучить модель после добавления intent + - чем тренировочный набор отличается от тестового + - как проверить данные перед обучением + - что именно запоминает модель при тренировке + - почему модель необходимо переобучать + - опиши подготовку корпуса к обучению + - какой результат создаёт процедура тренировки + - какие этапы проходит корпус перед созданием модели + - как размеченные примеры превращаются в классификатор + - зачем при обучении задают число эпох + - как собрать новый архив модели из данных + - что происходит до появления обученного артефакта + - объясни цикл train и validation + - как подготовить выборки для тренировки DIET + - от чего зависит сходимость обучения + - какой командой пересобрать модель после правок данных + - опиши путь от NLU-корпуса до файла модели + +- intent: ask_run + examples: | + - как запустить готового бота + - что сделать для старта ассистента + - какой командой открыть консольный чат + - как включить уже обученную модель + - подскажи порядок запуска программы + - как начать работу с помощником локально + - что нужно выполнить после получения модели + - как активировать диалоговый режим + - какой процесс обслуживает разговор с ботом + - как запустить сервер пользовательских действий + - что надо включить перед общением + - как проверить ассистента в командной строке + - опиши последовательность старта сервисов + - как загрузить сохранённую модель для диалога + - каким способом открыть интерактивный режим + - где вводить сообщения после запуска + - что запускается для проверки готового проекта + - как поднять action server отдельно + - как убедиться, что бот отвечает локально + - какая команда включает режим shell + - как использовать модель после тренировки + - расскажи о локальном старте приложения + - как инициировать беседу с ассистентом + - что делать, чтобы бот начал принимать фразы + - как выполнить пробный запуск системы + - как открыть shell после готового обучения + - в каком порядке включить actions и консольный чат + - где протестировать ответы сохранённой модели + - как использовать уже созданный архив модели + - что запустить для локального разговора с ботом + - как проверить готового помощника вручную + - объясни старт процессов после тренировки + - какой командой загрузить модель в интерактивном режиме + - как подключить action server при запуске shell + - что нужно включить, когда модель уже обучена + +- intent: ask_project_structure + examples: | + - как устроены файлы проекта + - покажи структуру каталогов + - где хранятся обучающие данные + - что содержится в domain yml + - в каком файле находятся пользовательские действия + - за что отвечает config yml + - где описаны диалоговые правила + - какой каталог содержит stories + - что лежит в папке actions + - где перечислены ответы бота + - в каком месте объявлены intents + - объясни назначение основных файлов + - где настроен NLU pipeline + - какой файл связывает action server + - где расположены тестовые сценарии + - как организована директория data + - что записано в endpoints yml + - где задаются slots и entities + - из чего состоит репозиторий + - покажи схему папок приложения + - какой файл содержит конфигурацию политик + - где найти исходный код custom actions + - как распределены настройки по YAML-файлам + - в какой папке лежат NLU-примеры + - опиши файловую организацию ассистента + - в каком YAML находятся шаблоны responses + - где расположен Python-код действий + - какой файл содержит примеры реплик пользователя + - покажи путь к настройкам slots + - где искать декларации пользовательских actions + - в каком каталоге сохранены правила диалога + - где находится конфигурация NLU-конвейера + - укажи расположение тестовых NLU-данных + - где в репозитории лежит domain + - как файлы распределены между data actions и tests + +- intent: ask_docker + examples: | + - что такое Docker + - зачем этому проекту контейнеры + - какую пользу даёт Docker Compose + - объясни принцип контейнеризации + - почему Rasa помещена в контейнер + - что изолирует Docker в приложении + - как связаны контейнер Rasa и action server + - для чего нужен compose-файл + - какие сервисы описаны в Docker Compose + - чем контейнер отличается от виртуальной машины + - почему контейнеры упрощают воспроизводимость + - какую среду предоставляет Docker + - как Docker фиксирует зависимости проекта + - что находится внутри образа Rasa + - зачем разделять бота и actions по сервисам + - как контейнеры общаются между собой + - какие порты используются сервисами + - что означает образ в Docker + - зачем в репозитории docker-compose yml + - как контейнеризация помогает переносу проекта + - что даёт закрепление версии образа + - объясни сетевое взаимодействие сервисов + - почему action server работает отдельно + - какую проблему окружения решает Docker + - расскажи об использовании Compose в архитектуре + - зачем сервисам нужна общая сеть Compose + - как контейнеры находят друг друга по имени + - что означает публикация порта контейнера + - почему actions вынесены в отдельный процесс + - как Rasa обращается к action server внутри сети + - зачем закреплять тег Docker-образа + - какие процессы изолированы разными сервисами + - объясни адрес action_server в Compose + - чем внутренний порт отличается от опубликованного + - как контейнеры делают запуск воспроизводимым + +- intent: feedback_bad + examples: | + - этот ответ мне не помог + - информация оказалась неверной + - ты неправильно понял мой вопрос + - полученный ответ бесполезен + - объяснение слишком непонятное + - результат меня не устраивает + - ответ не относится к моему запросу + - ты сообщил не то, что я спрашивал + - мне нужна более точная информация + - это объяснение не решает проблему + - ответ получился ошибочным + - я недоволен качеством ответа + - бот понял намерение неправильно + - предложенная информация не подходит + - ты выбрал неверную тему + - ответ оказался не по существу + - смысл моего сообщения распознан неверно + - это совсем не тот результат + - пояснение требует исправления + - ответь иначе, предыдущий вариант плохой + - мой запрос был о другом + - данная реакция ассистента ошибочна + - ты перепутал, что я хотел узнать + - такой ответ нельзя считать полезным + - необходимо дать более ясное объяснение + - система выбрала неправильное намерение + - этот ответ не соответствует смыслу вопроса + - результат распознавания требует исправления + - пояснение только сильнее запутало меня + - полученная реакция была неуместной + - ты неверно определил цель моего сообщения + - предложенный вариант оказался бесполезным + - ответ нужно сделать точнее и понятнее + - это не решило мой исходный вопрос + - бот ответил совсем на другую тему + +- intent: ask_time + examples: | + - который сейчас час + - сколько времени + - скажи текущее время + - подскажи время в Ташкенте + - какое сейчас время суток + - назови точное время + - сколько на часах + - сообщи местное время + - который час по Ташкенту + - хочу узнать время + - покажи часы + - можно узнать, сколько сейчас времени + - назови часы и минуты + - каково текущее ташкентское время + - сообщи время по UTC плюс пять + - что показывают часы сейчас + - укажи актуальное время + - мне нужно местное время + - подскажи, который теперь час + - какое время показывает система + - скажи время без даты + - сколько времени в часовом поясе Ташкента + - хочу сверить часы + - выведи текущие часы и минуты + - сообщи, который час в данный момент + - покажи только часы и минуты без календаря + - назови время на местных часах + - сообщи текущую временную отметку + - сколько часов и минут сейчас в городе + - мне нужны показания часов, а не дата + - укажи момент времени по Ташкенту + - покажи текущее значение часов + - выведи час и минуту текущего момента + - хочу проверить местные часы + - скажи точное время без числа и месяца + +- intent: ask_date + examples: | + - какая сегодня дата + - какое сегодня число + - назови текущую дату + - какой сейчас день месяца + - сообщи дату по Ташкенту + - какой сегодня день календаря + - укажи число и месяц + - хочу узнать сегодняшнюю дату + - покажи календарную дату + - назови день, месяц и год + - какое число на календаре + - сообщи актуальную дату + - что сегодня за дата + - какой год, месяц и число сейчас + - напомни сегодняшнее число + - дата на сегодня какая + - выведи текущий день календаря + - скажи дату без времени + - какой день месяца наступил + - укажи дату в Ташкенте + - что показывает календарь сегодня + - мне нужна дата этого дня + - назови полную сегодняшнюю дату + - какое сейчас календарное число + - сообщи день и месяц + - покажи только календарное число без часов + - назови сегодняшние день месяц и год + - какая дата стоит в местном календаре + - мне нужны сведения календаря, а не время + - укажи число текущего дня + - какой день отмечен сегодня в календаре + - сообщи год месяц и день на сегодня + - назови календарную отметку этого дня + - какое число наступило в Ташкенте + - выведи дату без часов и минут + +- intent: smalltalk_howareyou + examples: | + - как дела + - как ты себя чувствуешь + - какое у тебя настроение + - всё ли у тебя хорошо + - как поживаешь + - ты в порядке + - как проходит твой день + - что у тебя нового + - как твоё состояние + - всё работает нормально + - как жизнь, помощник + - не устал отвечать + - как сегодня настроение + - у тебя всё хорошо + - как идут дела у бота + - ты готов к разговору + - как себя ощущает ассистент + - надеюсь, у тебя всё отлично + - что скажешь о своём настроении + - как твои дела сегодня + - нормально себя чувствуешь + - система в хорошем состоянии + - как проходит работа + - ты сегодня бодр + - всё в порядке у тебя + - давно не общались, как поживает бот + - как обстановка в твоём цифровом мире + - каково состояние твоих систем сегодня + - расскажи, как проходит твоя работа сегодня + - в каком ты сейчас настроении + - как себя ведёт помощник в этот день + - всё спокойно у виртуального собеседника + - каково твоё самочувствие, бот + - есть ли у тебя новости о себе + - готов ли ты сегодня общаться + +- intent: smalltalk_joke + examples: | + - расскажи шутку + - пошути + - знаешь анекдот + - скажи что-нибудь смешное + - развесели меня + - хочу услышать шутку + - подними мне настроение юмором + - можешь рассказать анекдот + - выдай забавную фразу + - давай немного юмора + - рассмеши меня + - расскажи смешную историю + - есть хорошая шутка + - скажи забавный ответ + - хочу посмеяться + - поделись коротким анекдотом + - придумай что-нибудь весёлое + - время для шутки + - добавь юмора в разговор + - расскажи что-то забавное + - можешь меня развеселить + - пошути про программистов + - нужен смешной комментарий + - произнеси весёлую фразу + - есть повод улыбнуться + - удиви остроумным ответом + - устрой короткую минуту юмора + - скажи реплику, от которой можно улыбнуться + - добавь в беседу что-нибудь комичное + - хочу немного повеселиться + - придумай забавный комментарий + - развесели короткой историей + - поделись чем-нибудь остроумным + - сделай разговор веселее + - выдай порцию хорошего юмора + +- intent: out_of_scope + examples: | + - приготовь рецепт плова + - какой сегодня курс доллара + - назови столицу Японии + - переведи это предложение на английский + - реши квадратное уравнение + - посоветуй фильм на вечер + - закажи мне такси + - найди расписание поездов + - поставь медицинский диагноз + - кто победил в футбольном матче + - составь гороскоп для овна + - напиши стихотворение о море + - какую книгу почитать + - дай совет по отношениям + - сколько стоит новый ноутбук + - кто сейчас президент Франции + - нарисуй изображение кота + - отправь письмо преподавателю + - установи будильник на семь утра + - проложи маршрут до аэропорта + - стоит ли покупать акции + - объясни прогноз цены биткоина + - проверь мой юридический договор + - включи любимую музыку + - сколько калорий в пирожном + - подбери гостиницу в Самарканде + - расскажи новости за сегодня + - придумай пароль для аккаунта + - вызови мастера по ремонту + - найди ближайшую аптеку + - кто написал роман Война и мир + - вычисли интеграл функции + - помоги выбрать стиральную машину + - забронируй столик в ресторане + - какая команда выиграла чемпионат + - проверь орфографию моего сочинения + - расскажи о лечении простуды + - напомни купить продукты вечером + - узнай статус посылки + - составь план тренировок в спортзале + - сколько лететь до Москвы + - найди работу программистом + - определи песню по мелодии + - подскажи номер телефона банка + - рассчитай ежемесячный платёж по кредиту + - выбери подарок на день рождения + - покажи расписание кинотеатра + - закажи доставку еды + - расскажи правила игры в шахматы + - включи свет в комнате + - помоги починить домашний маршрутизатор + - расскажи биографию Николы Теслы + - составь экскурсию по музеям Испании + - напиши программу на Python для магазина + - создай слайды о Древнем Риме + - как запустить стиральную машину Bosch + - какое имя дать домашнему коту + - сколько времени варить картофель + - когда состоятся следующие выборы + - сообщи дату выхода нового смартфона + - объясни лечение аллергии у ребёнка + - найди прогноз погоды для Бухары + - помоги настроить Wi-Fi на телефоне + - расскажи о языке программирования Java + - кто является автором картины Мона Лиза + - создай новый аккаунт в Telegram + - предложи структуру курсовой по экономике + - обучи меня играть на фортепиано + - назови текущее население Германии + - придумай шутку про политического деятеля diff --git a/data/rules.yml b/data/rules.yml new file mode 100644 index 0000000..73062d8 --- /dev/null +++ b/data/rules.yml @@ -0,0 +1,113 @@ +version: "3.1" + +rules: +- rule: Приветствие + steps: + - intent: greet + - action: utter_greet + +- rule: Прощание + steps: + - intent: goodbye + - action: utter_goodbye + +- rule: Ответ на благодарность + steps: + - intent: thanks + - action: utter_you_are_welcome + +- rule: Показ возможностей + steps: + - intent: help + - action: action_show_capabilities + +- rule: Имя ассистента + steps: + - intent: ask_name + - action: utter_name + +- rule: Автор ассистента + steps: + - intent: ask_creator + - action: utter_creator + +- rule: Описание проекта + steps: + - intent: ask_project + - action: utter_project + +- rule: Сохранение имени пользователя + steps: + - intent: inform_name + - action: action_remember_name + +- rule: Перезапуск разговора + steps: + - intent: ask_restart + - action: utter_restart + - action: action_restart + +- rule: Объяснение Rasa + steps: + - intent: ask_rasa + - action: utter_rasa + +- rule: Объяснение NLU + steps: + - intent: ask_nlu + - action: utter_nlu + +- rule: Объяснение обучения + steps: + - intent: ask_training + - action: utter_training + +- rule: Объяснение запуска + steps: + - intent: ask_run + - action: utter_run + +- rule: Объяснение структуры проекта + steps: + - intent: ask_project_structure + - action: utter_project_structure + +- rule: Объяснение Docker + steps: + - intent: ask_docker + - action: utter_docker + +- rule: Отрицательная обратная связь + steps: + - intent: feedback_bad + - action: utter_feedback_bad + +- rule: Текущее время + steps: + - intent: ask_time + - action: action_show_time + +- rule: Текущая дата + steps: + - intent: ask_date + - action: action_show_date + +- rule: Ответ на вопрос о состоянии + steps: + - intent: smalltalk_howareyou + - action: utter_howareyou + +- rule: Небольшая шутка + steps: + - intent: smalltalk_joke + - action: utter_joke + +- rule: Известная неподдерживаемая тема + steps: + - intent: out_of_scope + - action: utter_out_of_scope + +- rule: Низкая уверенность NLU + steps: + - intent: nlu_fallback + - action: utter_default diff --git a/data/stories.yml b/data/stories.yml new file mode 100644 index 0000000..71a06bd --- /dev/null +++ b/data/stories.yml @@ -0,0 +1,93 @@ +version: "3.1" + +stories: +- story: Знакомство и обзор проекта + steps: + - intent: greet + - action: utter_greet + - intent: inform_name + entities: + - person_name: Хуршид + - action: action_remember_name + - intent: help + - action: action_show_capabilities + - intent: ask_project + - action: utter_project + +- story: Ассистент и его автор + steps: + - intent: ask_name + - action: utter_name + - intent: ask_creator + - action: utter_creator + - intent: thanks + - action: utter_you_are_welcome + +- story: От NLU к обучению + steps: + - intent: ask_rasa + - action: utter_rasa + - intent: ask_nlu + - action: utter_nlu + - intent: ask_training + - action: utter_training + +- story: Структура и запуск проекта + steps: + - intent: ask_project_structure + - action: utter_project_structure + - intent: ask_docker + - action: utter_docker + - intent: ask_run + - action: utter_run + +- story: Время и дата + steps: + - intent: ask_time + - action: action_show_time + - intent: ask_date + - action: action_show_date + - intent: thanks + - action: utter_you_are_welcome + +- story: Короткий разговор + steps: + - intent: smalltalk_howareyou + - action: utter_howareyou + - intent: smalltalk_joke + - action: utter_joke + - intent: goodbye + - action: utter_goodbye + +- story: Помощь после неудачного ответа + steps: + - intent: feedback_bad + - action: utter_feedback_bad + - intent: help + - action: action_show_capabilities + - intent: ask_nlu + - action: utter_nlu + +- story: Возврат из неподдерживаемой темы + steps: + - intent: out_of_scope + - action: utter_out_of_scope + - intent: ask_rasa + - action: utter_rasa + +- story: Восстановление после непонятного сообщения + steps: + - intent: nlu_fallback + - action: utter_default + - intent: help + - action: action_show_capabilities + +- story: Перезапуск именованного диалога + steps: + - intent: inform_name + entities: + - person_name: Анна + - action: action_remember_name + - intent: ask_restart + - action: utter_restart + - action: action_restart diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ff078f5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +# Проверка: docker compose run --rm --no-deps rasa data validate --fail-on-warnings +# Обучение: docker compose run --rm --no-deps rasa train +# CLI: docker compose run --rm rasa +services: + rasa: + image: rasa/rasa:3.6.21 + working_dir: /app + volumes: + - ./:/app + command: + - shell + - --endpoints + - endpoints.docker.yml + stdin_open: true + tty: true + depends_on: + action_server: + condition: service_healthy + environment: + RASA_TELEMETRY_ENABLED: "false" + + action_server: + image: rasa/rasa-sdk:3.6.2 + working_dir: /app + volumes: + - ./actions:/app/actions:ro + command: + - start + - --actions + - actions.actions + expose: + - "5055" + healthcheck: + test: + - CMD + - python + - -c + - import urllib.request; urllib.request.urlopen('http://localhost:5055/health', timeout=2) + interval: 5s + timeout: 3s + retries: 10 + start_period: 5s + environment: + RASA_TELEMETRY_ENABLED: "false" diff --git a/domain.yml b/domain.yml new file mode 100644 index 0000000..97384f3 --- /dev/null +++ b/domain.yml @@ -0,0 +1,104 @@ +version: "3.1" + +intents: + - greet + - goodbye + - thanks + - help + - ask_name + - ask_creator + - ask_project + - inform_name + - ask_restart + - ask_rasa + - ask_nlu + - ask_training + - ask_run + - ask_project_structure + - ask_docker + - feedback_bad + - ask_time + - ask_date + - smalltalk_howareyou + - smalltalk_joke + - out_of_scope + - nlu_fallback + +entities: + - person_name + +slots: + user_name: + type: text + influence_conversation: false + mappings: + - type: from_entity + entity: person_name + +responses: + utter_greet: + - text: "Здравствуйте! Я учебный русскоязычный помощник на Rasa. Могу рассказать о проекте, его ML-компонентах и запуске." + - text: "Привет! Спросите меня о проекте, Rasa, NLU, Docker, обучении модели или структуре репозитория." + + utter_goodbye: + - text: "До свидания! Спасибо за разговор." + - text: "До встречи! Буду рад снова ответить на вопросы о проекте." + + utter_you_are_welcome: + - text: "Пожалуйста! Рад быть полезным." + - text: "Не за что. Можем разобрать ещё один вопрос." + + utter_name: + - text: "Меня можно называть Rasa-помощником. Я демонстрирую распознавание русскоязычных намерений, извлечение сущностей и управление диалогом." + + utter_creator: + - text: "Меня создал Хуршид Мухаммадиев в рамках дипломного проекта о разработке диалоговой системы и виртуального помощника." + + utter_project: + - text: "Это учебный проект русскоязычного виртуального помощника. DIETClassifier определяет намерение и извлекает имя пользователя, а правила и диалоговые политики выбирают подготовленный ответ или действие." + + utter_restart: + - text: "Хорошо, начинаем заново. Состояние текущего диалога, включая сохранённое имя, будет очищено." + + utter_rasa: + - text: "Rasa Open Source — платформа для создания диалоговых систем. В этом проекте она объединяет NLU-модель, состояние диалога, правила, истории и пользовательские действия." + + utter_nlu: + - text: "NLU преобразует сообщение в структурированные данные. Intent описывает цель сообщения, а entity выделяет значимое значение: например, в фразе «Меня зовут Анна» intent — inform_name, а Анна — person_name." + + utter_training: + - text: "Сначала данные проверяются командой «rasa data validate», затем «rasa train» обучает NLU и диалоговые политики и сохраняет модель в каталоге models. Качество оценивается только на отдельных тестовых примерах." + + utter_run: + - text: "Для локального CLI запустите action server командой «rasa run actions», а в другом терминале — «rasa shell --endpoints endpoints.yml». В Docker используйте «docker compose run --rm --no-deps rasa train», затем запустите action_server и откройте shell с endpoints.docker.yml." + + utter_project_structure: + - text: "config.yml задаёт NLU-конвейер и политики; domain.yml — intents, entities, slots, ответы и actions; каталог data — обучающие примеры, rules и stories; actions содержит Python-код, а tests — независимые NLU-данные и Core-сценарии." + + utter_docker: + - text: "Docker фиксирует совместимые версии Rasa и Rasa SDK. Docker Compose создаёт общую сеть, в которой CLI-контейнер обращается к action server по имени сервиса." + + utter_feedback_bad: + - text: "Спасибо за замечание. Попробуйте переформулировать запрос или спросите, какие темы я поддерживаю." + + utter_howareyou: + - text: "Спасибо, всё работает штатно. Я готов отвечать на вопросы о проекте." + + utter_joke: + - text: "Почему разработчик доверяет логам больше догадок? Потому что у логов всегда есть запись разговора." + + utter_out_of_scope: + - text: "Я понял запрос, но эта тема находится за пределами моих возможностей. Я отвечаю на вопросы о проекте, Rasa, NLU, Docker, обучении и запуске." + + utter_default: + - text: "Я не уверен, что правильно понял сообщение. Переформулируйте вопрос или спросите о моих возможностях." + +actions: + - action_remember_name + - action_show_capabilities + - action_show_time + - action_show_date + +session_config: + session_expiration_time: 60 + carry_over_slots_to_new_session: false diff --git a/endpoints.docker.yml b/endpoints.docker.yml new file mode 100644 index 0000000..493872b --- /dev/null +++ b/endpoints.docker.yml @@ -0,0 +1,2 @@ +action_endpoint: + url: "http://action_server:5055/webhook" diff --git a/endpoints.yml b/endpoints.yml new file mode 100644 index 0000000..858a463 --- /dev/null +++ b/endpoints.yml @@ -0,0 +1,2 @@ +action_endpoint: + url: "http://127.0.0.1:5055/webhook" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..ee8c21e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==7.4.4 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5caa52d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +rasa==3.6.21 +rasa-sdk==3.6.2 diff --git a/results/README.md b/results/README.md new file mode 100644 index 0000000..9d4a07f --- /dev/null +++ b/results/README.md @@ -0,0 +1,54 @@ +# Evaluation results + +All committed metrics in this directory come from actual runs of the pinned +Rasa environment. Generated model archives are deliberately excluded from Git. +The current v2 procedure can be repeated from the tracked code, data, +configuration, and pinned direct dependencies; bit-for-bit model identity is +not claimed without a complete transitive lock file. + +## Baseline and final runs + +The v1 numbers in `experiments.md` are a historical baseline: its 565-example +corpus snapshot and raw JSON reports were not preserved, so that run is not +reproducible from this repository. Later diagnostic runs may be compared there +while improving only the training data or model configuration. A dataset that +has already been inspected during that process is diagnostic data, not an +untouched estimate of generalisation. + +`final/` contains the selected model's evaluation against the designated NLU +test file. The test split was held out from training, and neither train nor the +model configuration changed after the selected final result. If the test is +later used to guide another iteration, the next result must be described as +diagnostic rather than untouched. The summary also states that the manually +curated/synthetic corpus does not replace evaluation on messages from real +users. + +Run the reproducible pipeline from the repository root on Windows: + +```powershell +.\scripts\train.ps1 +.\scripts\evaluate.ps1 +``` + +`evaluate.ps1` defaults to `tests/nlu_test.yml` and `results/final/`; both paths +can be overridden with `-TestData` and `-OutputDirectory`. It runs the raw Rasa +NLU evaluation, measures the full pipeline with `FallbackClassifier`, evaluates +Core on gold intents using the same model, and then regenerates `summary.md` +from the JSON reports. + +The committed final artifacts are the real outputs used for review: + +- `intent_report.json` and `DIETClassifier_report.json` contain per-class and + aggregate metrics produced by Rasa; +- `intent_errors.json` and `DIETClassifier_errors.json` contain misclassified + examples used for error analysis; +- `*_confusion_matrix.png` and `*_histogram.png` are Rasa's diagnostic plots; +- `summary.md` is generated by `scripts/summarize_results.py` from those JSON + files; +- `runtime_report.json` stores aggregate coverage, fallback rate and selective + accuracy for the complete runtime pipeline without copying message texts. +- `story_report.json` is the Core-only report for ten dialogue scenarios with + gold intents; it is not an end-to-end NLU result. + +Successful-example dumps are intentionally not generated or committed. Never +replace generated metrics with estimates or manually invented values. diff --git a/results/experiments.md b/results/experiments.md new file mode 100644 index 0000000..c832386 --- /dev/null +++ b/results/experiments.md @@ -0,0 +1,62 @@ +# Журнал экспериментов + +Все значения ниже получены реальными запусками Rasa 3.6.21 на Python 3.10.11. +Для DIETClassifier и TEDPolicy задан `random_seed: 42`. Архивы моделей не +хранятся в Git. Текущую v2 и финальную процедуру можно повторить из сохранённых +данных, конфигурации и закреплённых прямых зависимостей; без полного +транзитивного lock-файла побитовая идентичность модели не гарантируется. + +Диагностический набор использовался для анализа ошибок между итерациями, +поэтому его результаты не являются финальной слепой оценкой. Он сохранён в +`tests/nlu_dev.yml`; итоговый отложенный набор — в `tests/nlu_test.yml`. + +| Итерация | Train | Diagnostic | Accuracy | Macro-F1 | Weighted-F1 | `person_name` token/tag F1 | +|---|---:|---:|---:|---:|---:|---:| +| v1: исходный очищенный корпус | 565 | 235 | 74,47% | 74,08% | 74,23% | 87,80% | +| v2: контрастное расширение данных | 795 | 235 | 85,11% | 84,96% | 84,85% | 87,80% | + +## Что изменилось в v2 + +- добавлено 230 новых обучающих примеров без точных пересечений с + диагностическим набором; +- усилены границы `ask_project` / `ask_creator`, `ask_training` / `ask_run`, + `ask_time` / `ask_date` и `ask_name` / `inform_name`; +- добавлены сложные отрицательные примеры для `out_of_scope`; +- расширены контексты извлечения неизвестного `person_name` и отрицательные + контексты с именами собственными; +- по сохранённому журналу конфигурация модели между v1 и v2 не менялась. + +В сохранённом историческом сравнении Macro-F1 вырос на **10,88 процентного +пункта**. Snapshot корпуса из 565 примеров и первичные JSON v1 не сохранились, +поэтому v1 служит исторической точкой отсчёта, но не считается воспроизводимым +артефактом этого репозитория. Наблюдаемый прирост согласуется с влиянием +расширения данных, но заново проверить это объяснение без snapshot v1 нельзя. + +Финальный test split был отложен от обучения. После результатов выбранного +финального прогона train и конфигурация модели не изменялись. Его артефакты +находятся в `results/final/`. + +## Финальная отложенная оценка + +- 235 test-сообщений; +- Raw intent accuracy: **89,36%**; +- Macro-F1 intent: **90,23%**; +- Weighted-F1 intent: **89,26%**; +- token/tag F1 `person_name`: **87,80%**; +- полный pipeline с fallback: **88,94% строгой accuracy**, **97,02% coverage** + принятых intent и **91,67% selective accuracy**. + +Fallback отклонил семь сообщений: шесть с ошибочным raw intent и одно с верным. +Он повысил точность среди принятых сообщений, но снизил строгую общую accuracy +на 0,43 процентного пункта и coverage на 2,98 процентного пункта. + +Статический валидатор подтверждает отсутствие точных пересечений между +train/dev/test, совпадающих train/test entity-шаблонов и повторно использованных +имён. По выбранной текстовой эвристике между train/test и dev/test нет пар +одного intent со score `>= 0,850`. Эта граница чувствительна к метрике и не +исключает семантически близкие формулировки; это техническая проверка, а не +доказательство статистической независимости корпуса. + +> Оба набора подготовлены вручную/синтетически. Эти метрики показывают качество +> на заданном корпусе и не заменяют тестирование на сообщениях реальных +> пользователей. diff --git a/results/final/DIETClassifier_confusion_matrix.png b/results/final/DIETClassifier_confusion_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..857b6db655da801fbc9feab2e35149d573f0016c GIT binary patch literal 37225 zcmeIb2UJv9);3&ftKBN1Em;9Ws~|}vO12Rr!2n7|Km5-w%=6au93FB z9tSHID+lv_En{N?BR)1Z-K7t(>fhC5D_}}2g`50ja8Aw$gCSK#|7{2s4${V89#@_} zeM;KgySKr@TT$kfaMOgT(=R`Fe%K=_d-@06njdI>ICanN$Gn5Bjyuo&c+V;NbD)2( zGq2;Wv8p^ww9_W1ZH`V%KOX#b>lQ`XeZMe7AF>zOwedVbsDr!V*^!@&gKz8MygP=B zdLu#=Wb{)j2%S6wC&i@Tg2zEu&FwJsdg$_(|5cRu(`WDEUBsVG9Oe9B{wKSyKap&h z|7pL`-}spSVW&5at*-p6&U(tmC1OMEJn1JX6z)XH-VO|Z1;8c=lw}N5!E`8sV$H6t<}A4#%l~KeBoYNoHT70B>DSdcxMJP)oWw4qiS!5iG{tJXtWwg%`cjWi%(?{8vKnk zE5Wd}<+TIdvHMvHdWnbi5}NI+?|dv!WAL_Ue-(wZoz5oA4G?l@bl$W7GBEUMJKJ-e z(aw^nU`yO|BPDjGmA*A?x_|GeQTx|ddp_POI40T>$4%Hm0bBLLl29dp!F>39q0D@* zZcNGJ^dcQ{0~>t43hSm!DeP34;Z#=;X?4!Xh#$c^(L_td;3OHH&1lJR$E7f)(?}uP34=Tby(2aC766C2qEyxR1kWBR&IJ zUyl0i+jB&mAu}XAzLBp#vBP(+r!?zZR4$#uT-wZVKI!Dw?R;hIf=9<9ibjfr>2v9^ z)+t>Q`p0nUH5wj|3V3h=wsRwX>AkP)wTB0@_DJ;-Y)gA=`;GjZJAAG5d?zkmwNA$u z=;sN&n~=2`t7wIlcX@zUIEOQ_RPL@x#?AI85ay=y8H=PvyNi01Cz7fyI(%9m9`&Uf zq`9tWD3&sjI=~X|EMXF8S!Q&r$hQGoko9m+{yoAjWaX+a@Zs5@>yDP zrtp|6a+xmF9PKB|1`GKIn0u;?iUhZoKNgyH+VlkF`yTbB5sFxXQ^tKlz4 zTZeM#=?zr2DPNHtE9GeSwd@X=oXizg7P9RQ3Cf8`?w7aMb7qN?wIJLl6_T}A(Doc? zd)G0GFT%PanR4YQ>DKMBZ%%NiN$L6RqN&ajN4<(8LEMa=9A|ad2Hp=@JfA5FP zytDwiUJy*fG6|j+Zb2NAS0>%#PR-`{NaAfc8OXM9hafX z__Q%{ofMFhd|eSz-X^4e#VHr&~#5k3mrXv&OLEoaGG0&yzcEczA~EtIwe zmvuXfoC54rjjL=Q$3XE#^)Ny~^OlQ?Q6g}|jUJMC$wf_zhGmyy*aA-o($%P>53Ma2F6$3njQQ@vedR}IW($ErNatj}*_zG}l*e!)+_|KrdVDZS*r zxH-FHV=eyocXpHVmT8`M3wq4@Kzh5?k(*DC^qN+yI@}F4`kDPExg+GfQ3_|s)Y zK@oiXans)FVN=ha#W!epj*4&4#8rm4369HB9}mQE%}R_tDAJyaEIKMQ^zg3DY>$fD zGfBRX6xIiCweJo35oS6z65LD8o;2$8S9$u43I=xNxY_io54oD9y!HM;Vvh??qq#a( z0#-KVMBTHEn2dw0x*V_Wr6pEL*$zLmhydwmnA(*2j?Li0GyCx8_kzhP14P($&1xtw zC|Py|XfSx(%iMgv?Dm(Z7g~~V)T-uczFW(jONbm^Fj+V9I zeHgt3hCd%td1fR428`k`GLEuY0maM`dka(BeJ}KU%sUpgBcXwEf1vh)MPuCn})0#5c|ijdVi@A=M;5#xos7HV{{qx>*-GL!xdtKhk-&yB1)_;J)&ur+x`m@@aE4sRpL z#1}eK_zR_S&myxsWQ#{sx=gN8CAGOLxYvaQ8jLy!�cPnerpx{n}c=nmzo3=Iit3 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�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 literal 0 HcmV?d00001 diff --git a/results/final/DIETClassifier_errors.json b/results/final/DIETClassifier_errors.json new file mode 100644 index 0000000..1179b2b --- /dev/null +++ b/results/final/DIETClassifier_errors.json @@ -0,0 +1,64 @@ +[ + { + "text": "На обращение Темур-Бек я откликнусь", + "entities": [ + { + "start": 13, + "end": 22, + "value": "Темур-Бек", + "entity": "person_name" + } + ], + "predicted_entities": [] + }, + { + "text": "Найди дешёвые билеты в Алматы", + "entities": [], + "predicted_entities": [ + { + "entity": "person_name", + "start": 23, + "end": 29, + "confidence_entity": 0.745909571647644, + "value": "Алматы", + "extractor": "DIETClassifier" + } + ] + }, + { + "text": "Как устроена файловая система Windows?", + "entities": [], + "predicted_entities": [ + { + "entity": "person_name", + "start": 30, + "end": 37, + "confidence_entity": 0.5979303121566772, + "value": "Windows", + "extractor": "DIETClassifier" + } + ] + }, + { + "text": "Какова дата рождения Александра Пушкина?", + "entities": [], + "predicted_entities": [ + { + "entity": "person_name", + "start": 21, + "end": 31, + "confidence_entity": 0.9825685024261475, + "value": "Александра", + "extractor": "DIETClassifier" + }, + { + "entity": "person_name", + "start": 32, + "end": 39, + "confidence_entity": 0.6506542563438416, + "value": "Пушкина", + "extractor": "DIETClassifier" + } + ] + } +] \ No newline at end of file diff --git a/results/final/DIETClassifier_histogram.png b/results/final/DIETClassifier_histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..364ef05bb2112cce89953ca5cfcd1113be87d5b0 GIT binary patch literal 40958 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̞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 literal 0 HcmV?d00001 diff --git a/results/final/DIETClassifier_report.json b/results/final/DIETClassifier_report.json new file mode 100644 index 0000000..cde46e4 --- /dev/null +++ b/results/final/DIETClassifier_report.json @@ -0,0 +1,28 @@ +{ + "person_name": { + "precision": 0.8181818181818182, + "recall": 0.9473684210526315, + "f1-score": 0.8780487804878049, + "support": 19, + "confused_with": {} + }, + "micro avg": { + "precision": 0.8181818181818182, + "recall": 0.9473684210526315, + "f1-score": 0.8780487804878049, + "support": 19 + }, + "macro avg": { + "precision": 0.8181818181818182, + "recall": 0.9473684210526315, + "f1-score": 0.8780487804878049, + "support": 19 + }, + "weighted avg": { + "precision": 0.8181818181818182, + "recall": 0.9473684210526315, + "f1-score": 0.8780487804878049, + "support": 19 + }, + "accuracy": 0.9954421148587056 +} \ No newline at end of file diff --git a/results/final/intent_confusion_matrix.png b/results/final/intent_confusion_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..1f55972c2cffd7036c19038b14cbfd2e15c72f4e GIT binary patch literal 101442 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 literal 0 HcmV?d00001 diff --git a/results/final/intent_errors.json b/results/final/intent_errors.json new file mode 100644 index 0000000..9636dc0 --- /dev/null +++ b/results/final/intent_errors.json @@ -0,0 +1,202 @@ +[ + { + "text": "Приятно встретиться с тобой этим утром", + "intent": "greet", + "intent_prediction": { + "name": "help", + "confidence": 0.9932712912559509 + } + }, + { + "text": "Я ценю оказанную поддержку", + "intent": "thanks", + "intent_prediction": { + "name": "help", + "confidence": 0.5510782599449158 + } + }, + { + "text": "Представься, пожалуйста", + "intent": "ask_name", + "intent_prediction": { + "name": "inform_name", + "confidence": 0.935112714767456 + } + }, + { + "text": "Чьей работой стало твоё появление?", + "intent": "ask_creator", + "intent_prediction": { + "name": "thanks", + "confidence": 0.5638586282730103 + } + }, + { + "text": "Можно ли проверить модель без повторного обучения?", + "intent": "ask_run", + "intent_prediction": { + "name": "ask_training", + "confidence": 0.9996166229248047 + } + }, + { + "text": "Какие каталоги есть у Rasa-проекта?", + "intent": "ask_project_structure", + "intent_prediction": { + "name": "ask_rasa", + "confidence": 0.500762403011322 + } + }, + { + "text": "Ты мне не помог", + "intent": "feedback_bad", + "intent_prediction": { + "name": "thanks", + "confidence": 0.795018196105957 + } + }, + { + "text": "В сообщённых сведениях есть ошибка", + "intent": "feedback_bad", + "intent_prediction": { + "name": "ask_date", + "confidence": 0.7298399209976196 + } + }, + { + "text": "Уже поздно или ещё рано?", + "intent": "ask_time", + "intent_prediction": { + "name": "greet", + "confidence": 0.5094811320304871 + } + }, + { + "text": "Определи, сейчас утро, день, вечер или ночь", + "intent": "ask_time", + "intent_prediction": { + "name": "greet", + "confidence": 0.5425835251808167 + } + }, + { + "text": "С тобой нынче всё ладно?", + "intent": "smalltalk_howareyou", + "intent_prediction": { + "name": "help", + "confidence": 0.9866999387741089 + } + }, + { + "text": "Ты как?", + "intent": "smalltalk_howareyou", + "intent_prediction": { + "name": "ask_name", + "confidence": 0.7040160298347473 + } + }, + { + "text": "В каком ты сегодня расположении духа?", + "intent": "smalltalk_howareyou", + "intent_prediction": { + "name": "ask_project_structure", + "confidence": 0.6958075761795044 + } + }, + { + "text": "Пошути, пожалуйста", + "intent": "smalltalk_joke", + "intent_prediction": { + "name": "inform_name", + "confidence": 0.4979470372200012 + } + }, + { + "text": "Разряди обстановку хорошей шуткой", + "intent": "smalltalk_joke", + "intent_prediction": { + "name": "smalltalk_howareyou", + "confidence": 0.5853334069252014 + } + }, + { + "text": "Сколько будет 347 умножить на 29?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "inform_name", + "confidence": 0.9305649995803833 + } + }, + { + "text": "Напиши поздравление с годовщиной свадьбы", + "intent": "out_of_scope", + "intent_prediction": { + "name": "greet", + "confidence": 0.3742619454860687 + } + }, + { + "text": "Как вылечить сильную боль в спине?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "ask_run", + "confidence": 0.46617835760116577 + } + }, + { + "text": "Объясни квантовую запутанность простыми словами", + "intent": "out_of_scope", + "intent_prediction": { + "name": "help", + "confidence": 0.635073721408844 + } + }, + { + "text": "Как завести автомобиль после морозной ночи?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "ask_run", + "confidence": 0.7760034799575806 + } + }, + { + "text": "Подскажи схему вязания тёплого шарфа спицами", + "intent": "out_of_scope", + "intent_prediction": { + "name": "help", + "confidence": 0.6222909688949585 + } + }, + { + "text": "Как устроена файловая система Windows?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "ask_project_structure", + "confidence": 0.9755270481109619 + } + }, + { + "text": "Что означает слово «раса» в биологии?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "ask_nlu", + "confidence": 0.9174363017082214 + } + }, + { + "text": "Как меня зовут?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "inform_name", + "confidence": 0.9968974590301514 + } + }, + { + "text": "Какова дата рождения Александра Пушкина?", + "intent": "out_of_scope", + "intent_prediction": { + "name": "inform_name", + "confidence": 0.7035830616950989 + } + } +] \ No newline at end of file diff --git a/results/final/intent_histogram.png b/results/final/intent_histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..6f06de891409d7a54f076014a3321a49f10ba9e8 GIT binary patch literal 42179 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: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 `rasa test nlu` запускает NLU-конвейер, но перед подсчётом intent-метрик восстанавливает исходный top-intent из ranking, если сработал FallbackClassifier. Поэтому итоговое fallback-решение отдельно измеряется полным runtime-конвейером. + +## Поведение полного pipeline + +- Raw accuracy до fallback: **89.36%** +- Строгая intent accuracy после fallback: **88.94%** +- Coverage — доля сообщений с принятым intent (не `nlu_fallback`): **97.02%** +- Selective accuracy среди принятых intent: **91.67%** +- Доля fallback: **2.98%** +- Fallback отклонил **7** сообщений: **1** с верным raw intent и **6** с ошибочным raw intent. + +> Это компромисс abstention: fallback может повысить точность среди принятых intent, одновременно снижая coverage и, если он отклоняет верный raw intent, строгую общую accuracy. + +## Rasa Core + +- Диалоговые истории: **10/10** +- Действия: **47/47** +- Action accuracy: **100.00%** + +> Это Core-only тесты с заранее заданными gold intents; NLU здесь не проверяется. Шаги `action_listen` составляют **23/47** всех проверенных действий. + +## Метрики по intent + +| Intent | Precision | Recall | F1 | Support | +|---|---:|---:|---:|---:| +| `smalltalk_howareyou` | 0.875 | 0.700 | 0.778 | 10 | +| `help` | 0.667 | 1.000 | 0.800 | 10 | +| `out_of_scope` | 1.000 | 0.667 | 0.800 | 30 | +| `greet` | 0.750 | 0.900 | 0.818 | 10 | +| `inform_name` | 0.750 | 1.000 | 0.857 | 15 | +| `ask_project_structure` | 0.818 | 0.900 | 0.857 | 10 | +| `ask_run` | 0.818 | 0.900 | 0.857 | 10 | +| `thanks` | 0.818 | 0.900 | 0.857 | 10 | +| `ask_time` | 1.000 | 0.800 | 0.889 | 10 | +| `feedback_bad` | 1.000 | 0.800 | 0.889 | 10 | +| `smalltalk_joke` | 1.000 | 0.800 | 0.889 | 10 | +| `ask_name` | 0.900 | 0.900 | 0.900 | 10 | +| `ask_creator` | 1.000 | 0.900 | 0.947 | 10 | +| `ask_date` | 0.909 | 1.000 | 0.952 | 10 | +| `ask_nlu` | 0.909 | 1.000 | 0.952 | 10 | +| `ask_rasa` | 0.909 | 1.000 | 0.952 | 10 | +| `ask_training` | 0.909 | 1.000 | 0.952 | 10 | +| `ask_docker` | 1.000 | 1.000 | 1.000 | 10 | +| `ask_project` | 1.000 | 1.000 | 1.000 | 10 | +| `ask_restart` | 1.000 | 1.000 | 1.000 | 10 | +| `goodbye` | 1.000 | 1.000 | 1.000 | 10 | + +## Частые смешения + +| Истинный intent | Предсказанный intent | Число | +|---|---|---:| +| `out_of_scope` | `inform_name` | 3 | +| `ask_time` | `greet` | 2 | +| `out_of_scope` | `ask_run` | 2 | +| `out_of_scope` | `help` | 2 | +| `greet` | `help` | 1 | +| `thanks` | `help` | 1 | +| `ask_name` | `inform_name` | 1 | +| `ask_creator` | `thanks` | 1 | +| `ask_run` | `ask_training` | 1 | +| `ask_project_structure` | `ask_rasa` | 1 | +| `feedback_bad` | `thanks` | 1 | +| `feedback_bad` | `ask_date` | 1 | +| `smalltalk_howareyou` | `help` | 1 | +| `smalltalk_howareyou` | `ask_name` | 1 | +| `smalltalk_howareyou` | `ask_project_structure` | 1 | + +> Финальный test-набор подготовлен отдельно и не использовался при обучении. Он остаётся вручную/синтетически составленным, поэтому результат не заменяет проверку на сообщениях реальных пользователей. diff --git a/scripts/chat.ps1 b/scripts/chat.ps1 new file mode 100644 index 0000000..c35fec7 --- /dev/null +++ b/scripts/chat.ps1 @@ -0,0 +1,105 @@ +[CmdletBinding()] +param( + [string]$Model +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$env:PYTHONUTF8 = "1" +$env:PYTHONIOENCODING = "utf-8" +$env:RASA_TELEMETRY_ENABLED = "false" + +$ProjectRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$Python = Join-Path $ProjectRoot ".venv-rasa\Scripts\python.exe" + +if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) { + throw @" +Rasa environment was not found at .venv-rasa. +Create it with Python 3.10 and install the pinned dependencies: + py -3.10 -m venv .venv-rasa + .\.venv-rasa\Scripts\python.exe -m pip install -r requirements.txt +"@ +} + +Set-Location -LiteralPath $ProjectRoot + +$EndpointsPath = Join-Path $ProjectRoot "endpoints.yml" +if (-not (Test-Path -LiteralPath $EndpointsPath -PathType Leaf)) { + throw "Required project file is missing: endpoints.yml" +} + +if ([string]::IsNullOrWhiteSpace($Model)) { + $ModelsDirectory = Join-Path $ProjectRoot "models" + if (-not (Test-Path -LiteralPath $ModelsDirectory -PathType Container)) { + throw "No models directory was found. Run scripts/train.ps1 first." + } + + $ModelFile = Get-ChildItem -LiteralPath $ModelsDirectory -Filter "*.tar.gz" -File | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 + + if ($null -eq $ModelFile) { + throw "No trained model was found in models/. Run scripts/train.ps1 first." + } + + $ModelPath = $ModelFile.FullName +} +else { + $ModelPath = (Resolve-Path -LiteralPath $Model).Path +} + +$ActionProcess = $null + +try { + Write-Host "Starting local Rasa action server on http://127.0.0.1:5055 ..." + $ActionProcess = Start-Process ` + -FilePath $Python ` + -ArgumentList @("-m", "rasa_sdk", "--actions", "actions.actions", "--port", "5055", "--quiet") ` + -WorkingDirectory $ProjectRoot ` + -PassThru ` + -WindowStyle Hidden + + $ActionServerReady = $false + for ($Attempt = 1; $Attempt -le 30; $Attempt++) { + if ($ActionProcess.HasExited) { + throw "The Rasa action server exited before becoming ready." + } + + try { + $HealthResponse = Invoke-WebRequest ` + -Uri "http://127.0.0.1:5055/health" ` + -UseBasicParsing ` + -TimeoutSec 1 + if ($HealthResponse.StatusCode -eq 200) { + $ActionServerReady = $true + break + } + } + catch { + Start-Sleep -Seconds 1 + } + } + + if (-not $ActionServerReady) { + throw "The Rasa action server did not become ready within 30 seconds." + } + + $RasaArguments = @( + "shell", + "--model", $ModelPath, + "--endpoints", $EndpointsPath + ) + + Write-Host "Running: rasa shell --model --endpoints " + & $Python -m rasa @RasaArguments + if ($LASTEXITCODE -ne 0) { + throw "Rasa shell failed with exit code $LASTEXITCODE." + } +} +finally { + if (($null -ne $ActionProcess) -and (-not $ActionProcess.HasExited)) { + Stop-Process -Id $ActionProcess.Id -Force -ErrorAction SilentlyContinue + $ActionProcess.WaitForExit() + } +} diff --git a/scripts/evaluate.ps1 b/scripts/evaluate.ps1 new file mode 100644 index 0000000..9177022 --- /dev/null +++ b/scripts/evaluate.ps1 @@ -0,0 +1,174 @@ +[CmdletBinding()] +param( + [string]$Model, + [string]$TestData = "tests/nlu_test.yml", + [string]$OutputDirectory = "results/final" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$env:PYTHONUTF8 = "1" +$env:PYTHONIOENCODING = "utf-8" +$env:RASA_TELEMETRY_ENABLED = "false" + +$ProjectRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$Python = Join-Path $ProjectRoot ".venv-rasa\Scripts\python.exe" + +if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) { + throw @" +Rasa environment was not found at .venv-rasa. +Create it with Python 3.10 and install the pinned development dependencies: + py -3.10 -m venv .venv-rasa + .\.venv-rasa\Scripts\python.exe -m pip install -r requirements-dev.txt +"@ +} + +Set-Location -LiteralPath $ProjectRoot + +$RequiredFiles = @( + "config.yml", + "domain.yml", + "data/nlu.yml", + "scripts/summarize_results.py", + "scripts/evaluate_runtime_nlu.py", + "tests/test_stories.yml" +) +foreach ($RequiredFile in $RequiredFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot $RequiredFile) -PathType Leaf)) { + throw "Required project file is missing: $RequiredFile" + } +} + +if ([string]::IsNullOrWhiteSpace($Model)) { + $ModelsDirectory = Join-Path $ProjectRoot "models" + if (-not (Test-Path -LiteralPath $ModelsDirectory -PathType Container)) { + throw "No models directory was found. Run scripts/train.ps1 first." + } + + $ModelFile = Get-ChildItem -LiteralPath $ModelsDirectory -Filter "*.tar.gz" -File | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 + + if ($null -eq $ModelFile) { + throw "No trained model was found in models/. Run scripts/train.ps1 first." + } + + $ModelPath = $ModelFile.FullName +} +else { + $ModelCandidate = if ([System.IO.Path]::IsPathRooted($Model)) { + $Model + } + else { + Join-Path $ProjectRoot $Model + } + + if (-not (Test-Path -LiteralPath $ModelCandidate -PathType Leaf)) { + throw "Model file was not found: $Model" + } + + $ModelPath = (Resolve-Path -LiteralPath $ModelCandidate).Path +} + +if ([string]::IsNullOrWhiteSpace($TestData)) { + throw "TestData must point to a Rasa NLU YAML file." +} + +$TestDataCandidate = if ([System.IO.Path]::IsPathRooted($TestData)) { + $TestData +} +else { + Join-Path $ProjectRoot $TestData +} + +if (-not (Test-Path -LiteralPath $TestDataCandidate -PathType Leaf)) { + throw "NLU test data file was not found: $TestData" +} +$TestDataPath = (Resolve-Path -LiteralPath $TestDataCandidate).Path + +if ([string]::IsNullOrWhiteSpace($OutputDirectory)) { + throw "OutputDirectory must not be empty." +} + +$OutputCandidate = if ([System.IO.Path]::IsPathRooted($OutputDirectory)) { + $OutputDirectory +} +else { + Join-Path $ProjectRoot $OutputDirectory +} +$OutputPath = [System.IO.Path]::GetFullPath($OutputCandidate) + +if (Test-Path -LiteralPath $OutputPath -PathType Leaf) { + throw "OutputDirectory points to a file: $OutputPath" +} +New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null +$OutputPath = (Resolve-Path -LiteralPath $OutputPath).Path + +$DomainPath = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot "domain.yml")).Path +$ConfigPath = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot "config.yml")).Path +$TrainDataPath = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot "data/nlu.yml")).Path +$SummarizerPath = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot "scripts/summarize_results.py")).Path +$RuntimeEvaluatorPath = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot "scripts/evaluate_runtime_nlu.py")).Path +$CoreStoriesPath = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot "tests/test_stories.yml")).Path +$SummaryPath = Join-Path $OutputPath "summary.md" +$RuntimeReportPath = Join-Path $OutputPath "runtime_report.json" +$CoreOutputPath = Join-Path $OutputPath "core" +$CoreReportPath = Join-Path $CoreOutputPath "story_report.json" +$PublishedCoreReportPath = Join-Path $OutputPath "story_report.json" + +$RasaArguments = @( + "test", + "nlu", + "--model", $ModelPath, + "--nlu", $TestDataPath, + "--domain", $DomainPath, + "--out", $OutputPath +) + +Write-Host "Running held-out NLU evaluation against $TestDataPath" +Write-Host "Running: rasa $($RasaArguments -join ' ')" +& $Python -m rasa @RasaArguments +if ($LASTEXITCODE -ne 0) { + throw "Rasa NLU evaluation failed with exit code $LASTEXITCODE." +} + +Write-Host "Measuring the complete NLU pipeline including FallbackClassifier" +& $Python $RuntimeEvaluatorPath ` + $ModelPath ` + --nlu $TestDataPath ` + --config $ConfigPath ` + --output $RuntimeReportPath +if ($LASTEXITCODE -ne 0) { + throw "Runtime NLU evaluation failed with exit code $LASTEXITCODE." +} + +Write-Host "Running Rasa Core evaluation against $CoreStoriesPath" +$CoreArguments = @( + "test", + "core", + "--model", $ModelPath, + "--stories", $CoreStoriesPath, + "--out", $CoreOutputPath +) +Write-Host "Running: rasa $($CoreArguments -join ' ')" +& $Python -m rasa @CoreArguments +if ($LASTEXITCODE -ne 0) { + throw "Rasa Core evaluation failed with exit code $LASTEXITCODE." +} +if (-not (Test-Path -LiteralPath $CoreReportPath -PathType Leaf)) { + throw "Rasa Core evaluation did not produce story_report.json." +} +Copy-Item -LiteralPath $CoreReportPath -Destination $PublishedCoreReportPath -Force + +Write-Host "Building Markdown summary from the generated reports" +& $Python $SummarizerPath ` + $OutputPath ` + --train-data $TrainDataPath ` + --test-data $TestDataPath ` + --output $SummaryPath +if ($LASTEXITCODE -ne 0) { + throw "Evaluation summary generation failed with exit code $LASTEXITCODE." +} + +Write-Host "Evaluation artifacts: $OutputPath" diff --git a/scripts/evaluate_runtime_nlu.py b/scripts/evaluate_runtime_nlu.py new file mode 100644 index 0000000..385899f --- /dev/null +++ b/scripts/evaluate_runtime_nlu.py @@ -0,0 +1,374 @@ +"""Evaluate the complete Rasa NLU pipeline, including fallback decisions. + +Unlike ``rasa test nlu``, this script sends every held-out example through a +loaded ``Agent``. The resulting report therefore reflects the configured +``FallbackClassifier`` and stores aggregates only, never example texts. +""" + +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import importlib.metadata +import json +import logging +import platform +from collections import Counter +from collections.abc import Mapping, Sequence +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import yaml + + +FALLBACK_INTENT = "nlu_fallback" +TOP_CONFUSIONS_LIMIT = 15 + + +class InputValidationError(ValueError): + """Raised when the evaluation inputs do not have the expected shape.""" + + +def _safe_ratio(numerator: int, denominator: int) -> float: + """Return a JSON-friendly ratio, including for an empty denominator.""" + + return numerator / denominator if denominator else 0.0 + + +def _examples_from_block(value: Any, context: str) -> list[str]: + """Extract annotated example strings from a Rasa YAML examples block.""" + + if isinstance(value, str): + examples: list[str] = [] + for line_number, line in enumerate(value.splitlines(), start=1): + if not line.strip(): + continue + stripped = line.strip() + if not stripped.startswith("-"): + raise InputValidationError( + f"{context}, line {line_number}: each multiline example must " + "start with '-'." + ) + example = stripped[1:].strip() + if not example: + raise InputValidationError( + f"{context}, line {line_number}: example text is empty." + ) + examples.append(example) + return examples + + if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + examples = [] + for example_index, item in enumerate(value, start=1): + if isinstance(item, str): + example = item.strip() + elif isinstance(item, Mapping): + text = item.get("text") + if not isinstance(text, str): + raise InputValidationError( + f"{context}, example {example_index}: list entries must " + "contain a string 'text' field." + ) + example = text.strip() + else: + raise InputValidationError( + f"{context}, example {example_index}: expected a string or " + "a mapping with a 'text' field." + ) + + if not example: + raise InputValidationError( + f"{context}, example {example_index}: example text is empty." + ) + examples.append(example) + return examples + + raise InputValidationError( + f"{context}: 'examples' must be a multiline string or a list." + ) + + +def load_labeled_examples(nlu_path: Path) -> list[tuple[str, str]]: + """Load ``(expected_intent, plain_text)`` pairs from Rasa NLU YAML.""" + + from rasa.shared.nlu.training_data.entities_parser import replace_entities + + try: + document = yaml.safe_load(nlu_path.read_text(encoding="utf-8")) + except UnicodeDecodeError as exc: + raise InputValidationError(f"NLU file is not valid UTF-8: {nlu_path}") from exc + except yaml.YAMLError as exc: + raise InputValidationError(f"Invalid YAML in {nlu_path}: {exc}") from exc + + if not isinstance(document, Mapping): + raise InputValidationError("NLU YAML root must be a mapping.") + + version = document.get("version") + if not isinstance(version, str) or not version.strip(): + raise InputValidationError("NLU YAML must contain a non-empty string 'version'.") + + nlu_blocks = document.get("nlu") + if not isinstance(nlu_blocks, list): + raise InputValidationError("NLU YAML field 'nlu' must be a list.") + if not nlu_blocks: + raise InputValidationError("NLU YAML field 'nlu' must not be empty.") + + labeled_examples: list[tuple[str, str]] = [] + for block_index, block in enumerate(nlu_blocks, start=1): + context = f"NLU block {block_index}" + if not isinstance(block, Mapping): + raise InputValidationError(f"{context} must be a mapping.") + + intent = block.get("intent") + if not isinstance(intent, str) or not intent.strip(): + raise InputValidationError( + f"{context} must contain a non-empty string 'intent'." + ) + intent = intent.strip() + if intent == FALLBACK_INTENT: + raise InputValidationError( + f"{context} uses reserved runtime intent '{FALLBACK_INTENT}' as " + "a gold label. Label the message with its expected domain intent " + "instead." + ) + + if "examples" not in block: + raise InputValidationError(f"{context} is missing 'examples'.") + annotated_examples = _examples_from_block(block["examples"], context) + if not annotated_examples: + raise InputValidationError(f"{context} contains no examples.") + + for annotated_text in annotated_examples: + plain_text = replace_entities(annotated_text).strip() + if not plain_text: + raise InputValidationError( + f"{context} contains an example that is empty after removing " + "entity markup." + ) + labeled_examples.append((intent, plain_text)) + + if not labeled_examples: + raise InputValidationError("NLU YAML contains no labeled examples.") + return labeled_examples + + +def _raw_candidate_name(parsed: Mapping[str, Any], predicted: str) -> str: + """Return the pre-fallback top intent for one runtime prediction.""" + + if predicted != FALLBACK_INTENT: + return predicted + + ranking = parsed.get("intent_ranking") + if not isinstance(ranking, Sequence) or isinstance(ranking, (str, bytes)): + raise RuntimeError( + "Rasa fallback prediction did not contain an intent_ranking sequence." + ) + + for candidate in ranking: + if not isinstance(candidate, Mapping): + continue + name = candidate.get("name") + if isinstance(name, str) and name and name != FALLBACK_INTENT: + return name + + raise RuntimeError( + "Rasa fallback prediction did not contain a raw non-fallback candidate." + ) + + +async def evaluate( + model_path: Path, labeled_examples: list[tuple[str, str]] +) -> dict[str, Any]: + """Run all examples through one loaded agent and aggregate predictions.""" + + from rasa.core.agent import Agent + + agent = Agent.load(str(model_path)) + if not agent.is_ready(): + raise RuntimeError(f"Rasa Agent could not load a ready model from {model_path}") + + correct = 0 + raw_correct = 0 + fallback_count = 0 + fallback_raw_correct_count = 0 + fallback_raw_incorrect_count = 0 + accepted_correct = 0 + fallback_expected: Counter[str] = Counter() + confusions: Counter[tuple[str, str]] = Counter() + + for expected, text in labeled_examples: + parsed = await agent.parse_message(text) + intent_data = parsed.get("intent") + if not isinstance(intent_data, Mapping): + raise RuntimeError("Rasa parser returned no intent mapping.") + predicted = intent_data.get("name") + if not isinstance(predicted, str) or not predicted: + raise RuntimeError("Rasa parser returned an invalid intent name.") + + raw_predicted = _raw_candidate_name(parsed, predicted) + is_correct = predicted == expected + is_raw_correct = raw_predicted == expected + if is_correct: + correct += 1 + if is_raw_correct: + raw_correct += 1 + + if predicted == FALLBACK_INTENT: + fallback_count += 1 + fallback_expected[expected] += 1 + if is_raw_correct: + fallback_raw_correct_count += 1 + else: + fallback_raw_incorrect_count += 1 + else: + if is_correct: + accepted_correct += 1 + + if not is_correct: + confusions[(expected, predicted)] += 1 + + total = len(labeled_examples) + accepted_count = total - fallback_count + ordered_confusions = sorted( + confusions.items(), key=lambda item: (-item[1], item[0][0], item[0][1]) + )[:TOP_CONFUSIONS_LIMIT] + ordered_fallback_distribution = sorted( + fallback_expected.items(), key=lambda item: (-item[1], item[0]) + ) + + return { + "total": total, + "correct": correct, + "accuracy": _safe_ratio(correct, total), + "raw_correct": raw_correct, + "raw_accuracy": _safe_ratio(raw_correct, total), + "fallback_count": fallback_count, + "fallback_raw_correct_count": fallback_raw_correct_count, + "fallback_raw_incorrect_count": fallback_raw_incorrect_count, + "fallback_rate": _safe_ratio(fallback_count, total), + "accepted_count": accepted_count, + "coverage": _safe_ratio(accepted_count, total), + "accepted_correct": accepted_correct, + "selective_accuracy": _safe_ratio(accepted_correct, accepted_count), + "fallback_expected_distribution": { + intent: count for intent, count in ordered_fallback_distribution + }, + "top_confusions": [ + {"expected": expected, "predicted": predicted, "count": count} + for (expected, predicted), count in ordered_confusions + ], + } + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Evaluate a full Rasa NLU pipeline, including FallbackClassifier, " + "without storing source example texts." + ) + ) + parser.add_argument("MODEL", type=Path, help="Path to a trained Rasa model") + parser.add_argument( + "--nlu", + type=Path, + default=Path("tests/nlu_test.yml"), + help="Held-out Rasa NLU YAML (default: tests/nlu_test.yml)", + ) + parser.add_argument( + "--config", + type=Path, + default=Path("config.yml"), + help="Rasa configuration file to fingerprint (default: config.yml)", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("results/final/runtime_report.json"), + help=( + "Aggregate JSON report path " + "(default: results/final/runtime_report.json)" + ), + ) + return parser + + +def _configure_rasa_logging() -> None: + """Keep Rasa from logging evaluated message texts at debug level.""" + + from rasa.utils.common import configure_logging_and_warnings + from rasa.utils.log_utils import configure_structlog + + configure_logging_and_warnings(log_level=logging.ERROR) + configure_structlog(log_level=logging.ERROR) + + +def _portable_path(path: Path) -> str: + """Return a repository-relative path without leaking a local home path.""" + + resolved = path.resolve() + try: + return resolved.relative_to(Path.cwd().resolve()).as_posix() + except ValueError: + return resolved.name + + +def _sha256(path: Path) -> str: + """Return the lowercase SHA-256 digest for a file.""" + + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main() -> int: + parser = _build_parser() + args = parser.parse_args() + + model_path: Path = args.MODEL.expanduser() + nlu_path: Path = args.nlu.expanduser() + config_path: Path = args.config.expanduser() + output_path: Path = args.output.expanduser() + + if not model_path.is_file(): + parser.error(f"model file does not exist: {model_path}") + if not nlu_path.is_file(): + parser.error(f"NLU file does not exist: {nlu_path}") + if not config_path.is_file(): + parser.error(f"config file does not exist: {config_path}") + + try: + labeled_examples = load_labeled_examples(nlu_path) + except (OSError, InputValidationError) as exc: + parser.error(str(exc)) + + _configure_rasa_logging() + report = asyncio.run(evaluate(model_path, labeled_examples)) + report["metadata"] = { + "model_file": _portable_path(model_path), + "model_sha256": _sha256(model_path), + "nlu_file": _portable_path(nlu_path), + "nlu_sha256": _sha256(nlu_path), + "config_file": _portable_path(config_path), + "config_sha256": _sha256(config_path), + "python_version": platform.python_version(), + "rasa_version": importlib.metadata.version("rasa"), + "rasa_sdk_version": importlib.metadata.version("rasa-sdk"), + "generated_at_utc": datetime.now(timezone.utc) + .isoformat(timespec="seconds") + .replace("+00:00", "Z"), + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + print(f"Wrote aggregate runtime NLU report to {output_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/summarize_results.py b/scripts/summarize_results.py new file mode 100644 index 0000000..ffc15f8 --- /dev/null +++ b/scripts/summarize_results.py @@ -0,0 +1,255 @@ +"""Create a compact Markdown summary from Rasa NLU evaluation artifacts.""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import platform +import sys +from collections import Counter +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import yaml + + +def load_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def percent(value: float) -> str: + return f"{value * 100:.2f}%" + + +def count_nlu_examples(path: Path) -> int: + document = yaml.safe_load(path.read_text(encoding="utf-8")) + blocks = document.get("nlu", []) if isinstance(document, dict) else [] + count = 0 + for block in blocks: + if not isinstance(block, dict) or "intent" not in block: + continue + examples = block.get("examples", "") + if isinstance(examples, str): + count += sum( + 1 for line in examples.splitlines() if line.strip().startswith("-") + ) + elif isinstance(examples, list): + count += len(examples) + return count + + +def package_version(name: str) -> str: + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return "не установлена" + + +def build_summary( + results_dir: Path, + train_data: Path, + test_data: Path, +) -> str: + intent_report = load_json(results_dir / "intent_report.json") + intent_errors = load_json(results_dir / "intent_errors.json") + entity_report = load_json(results_dir / "DIETClassifier_report.json") + entity_errors = load_json(results_dir / "DIETClassifier_errors.json") + + macro = intent_report["macro avg"] + weighted = intent_report["weighted avg"] + accuracy = intent_report["accuracy"] + support = int(macro["support"]) + + class_rows = [] + ignored = {"accuracy", "macro avg", "weighted avg", "micro avg"} + for name, metrics in intent_report.items(): + if name in ignored or not isinstance(metrics, dict): + continue + class_rows.append( + ( + name, + float(metrics["precision"]), + float(metrics["recall"]), + float(metrics["f1-score"]), + int(metrics["support"]), + ) + ) + class_rows.sort(key=lambda row: (row[3], row[0])) + + confusions = Counter( + (error["intent"], error["intent_prediction"]["name"]) + for error in intent_errors + ) + person_name_metrics = entity_report.get("person_name") + if not isinstance(person_name_metrics, dict): + # Backward-compatible fallback for older single-entity Rasa reports. + person_name_metrics = entity_report["macro avg"] + runtime_report_path = results_dir / "runtime_report.json" + runtime_report = ( + load_json(runtime_report_path) if runtime_report_path.is_file() else None + ) + core_report_path = results_dir / "story_report.json" + core_report = load_json(core_report_path) if core_report_path.is_file() else None + train_count = count_nlu_examples(train_data) + test_count = count_nlu_examples(test_data) + + lines = [ + "# Итоги NLU-оценки", + "", + f"- Дата запуска (UTC): **{datetime.now(timezone.utc):%Y-%m-%d}**", + f"- Окружение: **Python {platform.python_version()}, " + f"Rasa {package_version('rasa')}, Rasa SDK {package_version('rasa-sdk')}**", + f"- Данные: **{train_count} train / {test_count} test сообщений**", + "- Конфигурация: **DIETClassifier/TEDPolicy, 100 эпох, seed 42**", + f"- Оценено сообщений: **{support}**", + f"- Accuracy: **{percent(accuracy)}**", + f"- Macro-F1: **{percent(macro['f1-score'])}**", + f"- Weighted-F1: **{percent(weighted['f1-score'])}**", + f"- Ошибок intent: **{len(intent_errors)}**", + f"- F1 `person_name` на уровне токенов/тегов: " + f"**{percent(person_name_metrics['f1-score'])}**", + f"- Сообщений с ошибкой entity: **{len(entity_errors)}**", + "", + "> `rasa test nlu` запускает NLU-конвейер, но перед подсчётом " + "intent-метрик восстанавливает исходный top-intent из ranking, если " + "сработал FallbackClassifier. Поэтому итоговое fallback-решение отдельно " + "измеряется полным runtime-конвейером.", + "", + ] + + if isinstance(runtime_report, dict): + raw_accuracy = float(runtime_report.get("raw_accuracy", accuracy)) + fallback_count = int(runtime_report.get("fallback_count", 0)) + fallback_raw_correct = runtime_report.get("fallback_raw_correct_count") + fallback_raw_incorrect = runtime_report.get("fallback_raw_incorrect_count") + lines.extend( + [ + "## Поведение полного pipeline", + "", + f"- Raw accuracy до fallback: **{percent(raw_accuracy)}**", + f"- Строгая intent accuracy после fallback: " + f"**{percent(float(runtime_report['accuracy']))}**", + f"- Coverage — доля сообщений с принятым intent " + f"(не `nlu_fallback`): " + f"**{percent(float(runtime_report['coverage']))}**", + f"- Selective accuracy среди принятых intent: " + f"**{percent(float(runtime_report['selective_accuracy']))}**", + f"- Доля fallback: " + f"**{percent(float(runtime_report['fallback_rate']))}**", + ] + ) + if fallback_raw_correct is not None and fallback_raw_incorrect is not None: + lines.append( + f"- Fallback отклонил **{fallback_count}** сообщений: " + f"**{int(fallback_raw_correct)}** с верным raw intent и " + f"**{int(fallback_raw_incorrect)}** с ошибочным raw intent." + ) + lines.extend( + [ + "", + "> Это компромисс abstention: fallback может повысить точность " + "среди принятых intent, одновременно снижая coverage и, если он " + "отклоняет верный raw intent, строгую общую accuracy.", + "", + ] + ) + + if isinstance(core_report, dict): + conversation = core_report.get("conversation_accuracy", {}) + total_actions = int(core_report["macro avg"]["support"]) + correct_actions = round(float(core_report["accuracy"]) * total_actions) + action_listen = core_report.get("action_listen") + action_listen_support = ( + int(action_listen.get("support", 0)) + if isinstance(action_listen, dict) + else None + ) + lines.extend( + [ + "## Rasa Core", + "", + f"- Диалоговые истории: **{conversation.get('correct', 0)}/" + f"{conversation.get('total', 0)}**", + f"- Действия: **{correct_actions}/{total_actions}**", + f"- Action accuracy: **{percent(float(core_report['accuracy']))}**", + ] + ) + core_caveat = ( + "> Это Core-only тесты с заранее заданными gold intents; NLU здесь " + "не проверяется." + ) + if action_listen_support is not None: + core_caveat += ( + f" Шаги `action_listen` составляют " + f"**{action_listen_support}/{total_actions}** всех проверенных " + "действий." + ) + lines.extend(["", core_caveat, ""]) + + lines.extend( + [ + "## Метрики по intent", + "", + "| Intent | Precision | Recall | F1 | Support |", + "|---|---:|---:|---:|---:|", + ] + ) + lines.extend( + f"| `{name}` | {precision:.3f} | {recall:.3f} | {f1:.3f} | {count} |" + for name, precision, recall, f1, count in class_rows + ) + + lines.extend( + [ + "", + "## Частые смешения", + "", + "| Истинный intent | Предсказанный intent | Число |", + "|---|---|---:|", + ] + ) + lines.extend( + f"| `{expected}` | `{predicted}` | {count} |" + for (expected, predicted), count in confusions.most_common(15) + ) + + lines.extend( + [ + "", + "> Финальный test-набор подготовлен отдельно и не использовался " + "при обучении. Он остаётся вручную/синтетически составленным, " + "поэтому результат не заменяет проверку на сообщениях реальных " + "пользователей.", + "", + ] + ) + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("results_dir", nargs="?", default="results/final") + parser.add_argument("--output", help="Optional Markdown output path") + parser.add_argument("--train-data", default="data/nlu.yml") + parser.add_argument("--test-data", default="tests/nlu_test.yml") + args = parser.parse_args() + + summary = build_summary( + Path(args.results_dir), + Path(args.train_data), + Path(args.test_data), + ) + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + print(summary) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(summary, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/train.ps1 b/scripts/train.ps1 new file mode 100644 index 0000000..682c616 --- /dev/null +++ b/scripts/train.ps1 @@ -0,0 +1,61 @@ +[CmdletBinding()] +param( + [switch]$Force +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$env:PYTHONUTF8 = "1" +$env:PYTHONIOENCODING = "utf-8" +$env:RASA_TELEMETRY_ENABLED = "false" + +$ProjectRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$Python = Join-Path $ProjectRoot ".venv-rasa\Scripts\python.exe" + +if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) { + throw @" +Rasa environment was not found at .venv-rasa. +Create it with Python 3.10 and install the pinned dependencies: + py -3.10 -m venv .venv-rasa + .\.venv-rasa\Scripts\python.exe -m pip install -r requirements.txt +"@ +} + +Set-Location -LiteralPath $ProjectRoot + +$RequiredFiles = @("config.yml", "domain.yml", "endpoints.yml") +foreach ($RequiredFile in $RequiredFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot $RequiredFile) -PathType Leaf)) { + throw "Required project file is missing: $RequiredFile" + } +} + +$RasaArguments = @( + "train", + "--config", "config.yml", + "--domain", "domain.yml", + "--data", "data", + "--out", "models", + "--endpoints", "endpoints.yml" +) + +if ($Force) { + $RasaArguments += "--force" +} + +Write-Host "Running: rasa $($RasaArguments -join ' ')" +& $Python -m rasa @RasaArguments +if ($LASTEXITCODE -ne 0) { + throw "Rasa training failed with exit code $LASTEXITCODE." +} + +$LatestModel = Get-ChildItem -LiteralPath (Join-Path $ProjectRoot "models") -Filter "*.tar.gz" -File | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 + +if ($null -eq $LatestModel) { + throw "Training completed without producing a model in models/." +} + +Write-Host "Model created: $($LatestModel.FullName)" diff --git a/scripts/validate.ps1 b/scripts/validate.ps1 new file mode 100644 index 0000000..fe21b29 --- /dev/null +++ b/scripts/validate.ps1 @@ -0,0 +1,53 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$env:PYTHONUTF8 = "1" +$env:PYTHONIOENCODING = "utf-8" +$env:RASA_TELEMETRY_ENABLED = "false" + +$ProjectRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$Python = Join-Path $ProjectRoot ".venv-rasa\Scripts\python.exe" + +if (-not (Test-Path -LiteralPath $Python -PathType Leaf)) { + throw @" +Rasa environment was not found at .venv-rasa. +Create it with Python 3.10 and install the pinned development dependencies: + py -3.10 -m venv .venv-rasa + .\.venv-rasa\Scripts\python.exe -m pip install -r requirements-dev.txt +"@ +} + +Set-Location -LiteralPath $ProjectRoot + +$RequiredFiles = @( + "config.yml", + "domain.yml", + "endpoints.yml", + "tests/static_validate.py" +) + +foreach ($RequiredFile in $RequiredFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot $RequiredFile) -PathType Leaf)) { + throw "Required project file is missing: $RequiredFile" + } +} + +Write-Host "Running: rasa data validate --domain domain.yml --data data --fail-on-warnings" +& $Python -m rasa data validate ` + --domain "domain.yml" ` + --data "data" ` + --fail-on-warnings +if ($LASTEXITCODE -ne 0) { + throw "Rasa data validation failed with exit code $LASTEXITCODE." +} + +Write-Host "Running: python tests/static_validate.py" +& $Python "tests/static_validate.py" +if ($LASTEXITCODE -ne 0) { + throw "Static project validation failed with exit code $LASTEXITCODE." +} + +Write-Host "Validation completed successfully." diff --git a/tests/nlu_dev.yml b/tests/nlu_dev.yml new file mode 100644 index 0000000..786a7a7 --- /dev/null +++ b/tests/nlu_dev.yml @@ -0,0 +1,302 @@ +version: "3.1" + +# Диагностический набор использовался для анализа ошибок между итерациями. +# Финальная отложенная оценка находится в tests/nlu_test.yml. +nlu: +- intent: greet + examples: | + - здравия и успехов + - моё почтение + - приветствую вас в новом диалоге + - начнём со знакомства + - здравия желаю + - хочу начать с приветствия + - приятно встретиться + - начинаю разговор с приветствия + - алло, помощник, ты здесь + - на связи новый пользователь + +- intent: goodbye + examples: | + - благодарю за беседу, я завершаю работу + - пора заканчивать нашу встречу + - оставим вопросы до другого раза + - желаю удачи, до нового разговора + - я больше не буду тебя задерживать + - можно закрыть беседу + - увидимся в следующем сеансе + - откланиваюсь + - на сегодня закончили + - покидаю чат + +- intent: thanks + examples: | + - твоя подсказка пригодилась + - я получил нужное объяснение, благодарю + - ты здорово меня выручил + - теперь всё ясно, моя благодарность + - признателен за содержательный разбор + - помощь пришлась очень кстати + - благодарю, вопрос решён + - ценная подсказка, спасибо тебе + - ответ оказался полезным, большое спасибо + - выражаю признательность за содействие + +- intent: help + examples: | + - сориентируй меня по доступным разделам + - не знаю, с чего начать общение + - предложи темы для разговора + - проведи краткий обзор своих навыков + - какие операции доступны пользователю + - где посмотреть руководство по боту + - объясни правила взаимодействия с системой + - что входит в твою компетенцию + - покажи меню функций + - какие задачи тебе можно поручить + +- intent: ask_name + examples: | + - как подписан мой виртуальный собеседник + - сообщи своё обозначение + - представь себя пользователю + - каково имя системы + - хотелось бы знать, с кем ведётся беседа + - какое обращение ты предпочитаешь + - кем мне считать отвечающего помощника + - подскажи название бота + - идентифицируй себя + - кто находится на другом конце диалога + +- intent: ask_creator + examples: | + - назови человека, выполнившего разработку + - чья работа лежит в основе ассистента + - укажи создателя программного решения + - кем написана эта система + - кто подготовил исходный код + - хочу узнать исполнителя дипломной работы + - сообщи фамилию автора бота + - кто стоял за созданием помощника + - какой разработчик реализовал проект + - назови человека, которому принадлежит эта разработка + +- intent: ask_project + examples: | + - сформулируй основную идею выполненной работы + - какой замысел лежит в основе ассистента + - опиши предмет дипломного исследования + - какую пользу должна показать разработка + - что было главной целью автора проекта + - дай общее резюме выполненной системы + - какие результаты представляет этот диплом + - в рамках какой темы создан помощник + - раскрой суть проектной работы + - какую концепцию диалоговых систем здесь исследуют + +- intent: inform_name + examples: | + - для начала запомни, что собеседника зовут [Максим](person_name) + - в карточке пользователя укажи [Ольга](person_name) + - подпиши меня как [Сергей Белов](person_name) + - я зарегистрирована под именем [Юлия Андреева](person_name) + - правильное написание моего имени — [Нур-Мухаммад](person_name) + - задай мне обращение [Анастасия](person_name) + - имя для этого сеанса: [Вадим](person_name) + - пожалуйста, считай моим именем [Гульнара Каримова](person_name) + - правильное обращение ко мне — [Пётр Алексеевич](person_name) + - мой собеседник может называть меня [Эмиль](person_name) + - внеси в память имя [Ксения](person_name) + - я отзываюсь на имя [Андрей Соколов](person_name) + - пусть в разговоре моим именем будет [Лейла](person_name) + - для обращения подойдёт [Степан](person_name) + - зафиксируй для беседы имя [Майя-Лин](person_name) + +- intent: ask_restart + examples: | + - создай беседу без предыдущих сообщений + - требуется полностью обновить состояние чата + - можешь забыть всё сказанное ранее + - открой для меня чистый сеанс + - хочу сбросить сохранённый ход общения + - верни бота в состояние первого запуска + - удали накопленный контекст этой переписки + - переключи нас на новую беседу + - перезапуск нужен без сохранения истории + - очистим сведения из текущего разговора + +- intent: ask_rasa + examples: | + - к какому типу программ относится Rasa + - расскажи, что представляет собой этот фреймворк + - почему для диалогового помощника взяли платформу Rasa + - какое место занимает Rasa в общей системе + - перечисли ключевые части фреймворка + - для каких проектов подходит Rasa Open Source + - в чём преимущество Rasa для учебного ассистента + - охарактеризуй платформу, на которой построен бот + - какие возможности предоставляет сама Rasa + - объясни назначение фреймворка без деталей обучения + +- intent: ask_nlu + examples: | + - каким способом фраза получает метку намерения + - объясни разницу между целью запроса и извлечённым значением + - что делает модуль понимания пользовательской речи + - каким образом предложение относят к нужному классу + - зачем отмечать имена внутри обучающих фраз + - какой этап отвечает за смысловой разбор текста + - что возвращает анализатор языка диалоговому менеджеру + - как распознаются полезные значения в сообщении + - раскрой понятия intent и entity на примере + - по какому принципу бот понимает категорию реплики + +- intent: ask_training + examples: | + - опиши, как из размеченного корпуса получается классификатор + - что алгоритм делает с примерами во время тренировки + - когда требуется заново построить модель + - каким должен быть набор данных для качественного обучения + - где настраиваются параметры тренировочного процесса + - как обучающие фразы используются компонентом DIET + - что создаётся после завершения процедуры обучения + - почему нельзя оценивать модель на её тренировочных данных + - какие шаги проходит подготовка модели к работе + - как изменится обучение после добавления нового класса + +- intent: ask_run + examples: | + - как перейти от готового архива модели к разговору + - подскажи команду для интерактивной проверки бота + - что включить, чтобы принимать сообщения в консоли + - каким образом поднять уже подготовленный ассистент + - как открыть режим непосредственного общения + - опиши запуск компонентов после обучения + - где проверить ответы работающей модели + - как отдельно стартовать исполнитель custom actions + - что необходимо запустить для локальной демонстрации + - как проверить систему вручную через shell + +- intent: ask_project_structure + examples: | + - нарисуй карту директорий репозитория + - в какой части проекта объявлена сущность имени + - откуда Rasa читает примеры пользовательских фраз + - найди файл с шаблонами ответов + - где располагается реализация действий на Python + - какой конфигурационный файл описывает компоненты NLU + - поясни распределение кода и данных по каталогам + - в каком документе указан адрес сервера actions + - где искать правила переходов между репликами + - какие основные элементы находятся в корне проекта + +- intent: ask_docker + examples: | + - почему окружение проекта упаковано в образы + - какую задачу выполняет Compose при сборке сервисов + - поясни разделение приложения на два контейнера + - как контейнеризация обеспечивает одинаковый запуск + - для чего сервисам внутренняя сеть Docker + - что означает зафиксированный тег образа + - где описаны зависимости контейнерного окружения + - почему actions не находятся в одном процессе с Rasa + - какую роль играют проброшенные порты + - расскажи, как Compose объединяет части системы + +- intent: feedback_bad + examples: | + - предыдущая реакция не соответствует смыслу фразы + - я ожидал совсем другое объяснение + - исправь результат, он получился неточным + - классификация моего запроса явно ошибочна + - приведённые сведения не отвечают на вопрос + - такой ответ только запутывает + - помощник выбрал неподходящий вариант реакции + - качество объяснения меня разочаровало + - повтори по-другому, сейчас ничего не ясно + - ты неверно определил, чего я хотел + +- intent: ask_time + examples: | + - можно свериться с ташкентскими часами + - выведи показание времени для UTC+5 + - сколько минут после текущего часа + - мне нужны часы на данный момент + - сообщи временную отметку без календаря + - что сейчас показывают часы в городе + - нужна временная отметка по местному поясу + - подскажи актуальные показания часов + - узнай время для ташкентского пояса + - какой час наступил прямо сейчас + +- intent: ask_date + examples: | + - какой лист календаря соответствует этому дню + - перечисли нынешние число, месяц и год + - сообщи календарные данные на данный момент + - что за число сегодня по местному календарю + - напомни месяц и день сегодняшнего дня + - выведи дату для часового пояса Ташкента + - какое число следует записать в документе сегодня + - назови сегодняшние календарные реквизиты + - какой датой отмечен текущий день + - мне требуется число этого месяца и год + +- intent: smalltalk_howareyou + examples: | + - как проходит твоя виртуальная жизнь + - готов ли помощник сегодня работать + - расскажи о своём самочувствии + - как складывается твой рабочий день + - давно не виделись, как ты там + - надеюсь, система чувствует себя прекрасно + - какое настроение у моего собеседника + - всё ли спокойно в твоём цифровом мире + - как ты поживал без меня + - бот сегодня в хорошем расположении духа + +- intent: smalltalk_joke + examples: | + - удиви меня остроумной репликой + - есть ли у тебя забавный анекдот для беседы + - устрой небольшую минуту смеха + - хочу проверить твоё чувство юмора + - поделись весёлой историей про код + - произнеси что-либо, способное рассмешить + - разряди обстановку забавным ответом + - подкинь хорошую шутейку + - пора добавить в диалог немного веселья + - умеешь смешно шутить + +- intent: out_of_scope + examples: | + - какая температура будет завтра утром + - подбери авиабилеты до Дубая + - научи меня играть на гитаре + - составь меню на неделю + - найди результаты теннисного турнира + - сколько белка содержится в курице + - объясни правила получения визы + - переведи деньги на другую карту + - расскажи последние политические события + - помоги выбрать университет для поступления + - напиши программу сортировки на Java + - какая планета ближе всего к Солнцу + - отредактируй фотографию для паспорта + - найди дешёвый мобильный тариф + - составь маршрут путешествия по Италии + - когда приедет автобус номер двадцать четыре + - подскажи упражнения для больной спины + - создай презентацию по истории + - сколько будет сто сорок семь умножить на восемь + - закажи букет цветов с доставкой + - проверь наличие товара в магазине + - подбери рецепт без глютена + - расскажи биографию Альберта Эйнштейна + - найди прогноз землетрясений + - помоги настроить домашний роутер + - сочини поздравление с юбилеем + - определи породу собаки по описанию + - узнай результаты анализов в клинике + - рассчитай налог с заработной платы + - включи кондиционер на двадцать градусов diff --git a/tests/nlu_test.yml b/tests/nlu_test.yml new file mode 100644 index 0000000..8226b5c --- /dev/null +++ b/tests/nlu_test.yml @@ -0,0 +1,302 @@ +version: "3.1" + +# Отложенный набор не входит в train; его результаты не использовались для +# настройки финальной модели. Пересечения и пороговые метрики проверяются статически. +nlu: + - intent: greet + examples: | + - Я заглянул поздороваться — приветствую! + - Приятно встретиться с тобой этим утром + - Здравствуйте, рад начать беседу + - Приветствую тебя + - Рад нашей вечерней встрече, помощник + - Хей, давай поговорим + - Ну здравствуй, цифровой собеседник + - Всем доброго дня + - Я здесь, начнём? + - Здорово, помощник + + - intent: goodbye + examples: | + - Буду прощаться, счастливо оставаться + - Пока, спасибо за беседу + - На этом закончим + - Мне пора, до встречи + - На сегодня всё, хорошего дня + - Увидимся в следующий раз + - Завершаю разговор + - Закрываю чат до нашей следующей беседы + - Ладно, я ухожу + - Можно прощаться + + - intent: thanks + examples: | + - Ты выручил, премного благодарен + - Большое спасибо за ответ + - Прими мою искреннюю благодарность + - Я ценю оказанную поддержку + - Ты здорово помог + - После твоего ответа всё прояснилось, благодарю + - Твоя помощь пришлась кстати, спасибо + - Большое человеческое спасибо тебе + - Признателен за такое понятное объяснение + - Ты дал полезную подсказку, огромное спасибо + + - intent: help + examples: | + - Озвучь круг задач, с которыми к тебе можно прийти + - Покажи список возможностей + - Как тобой пользоваться? + - С какими вопросами можно обращаться? + - Подскажи доступные команды + - Мне нужна помощь по работе с ботом + - С чего начать диалог с тобой? + - Расскажи, чем можешь помочь + - Какие функции у тебя есть? + - Дай краткую инструкцию по общению + + - intent: ask_name + examples: | + - Какое имя подставить вместо обращения «бот»? + - У виртуального собеседника предусмотрено имя? + - Представься, пожалуйста + - Кто ты такой? + - Какое обращение к тебе будет правильным? + - Сообщи своё имя как бота + - Ты какой бот? + - Можно узнать твоё имя? + - Каким именем ты представляешься пользователям? + - Расскажи о своей идентичности + + - intent: ask_creator + examples: | + - Кому принадлежит разработка этого помощника? + - Кто твой разработчик? + - Назови автора этого бота + - Чьей работой стало твоё появление? + - Кто работал над твоим созданием? + - Чей это программный проект? + - Кто написал твою логику? + - Что можешь сообщить об авторе своей разработки? + - Как зовут разработчика системы? + - Кто является автором помощника? + + - intent: ask_project + examples: | + - В чём цель дипломного проекта? + - Для чего создан этот дипломный бот? + - Каково назначение проекта? + - Какую задачу решает эта дипломная работа? + - Расскажи об идее проекта + - Зачем понадобилась эта система? + - Какой результат должен показать диплом? + - Какую практическую идею диплома воплощает этот бот? + - Какова практическая цель разработки? + - О чём этот дипломный проект? + + - intent: inform_name + examples: | + - Разреши представиться: я [Анна](person_name) + - Если понадобится обратиться лично, моё имя — [Мехринисо](person_name) + - Для начала представлюсь — [Дилноза](person_name) + - Моё имя для общения — [Алексей](person_name) + - При знакомстве я представляюсь как [Малика](person_name) + - Перед тобой [Евгений](person_name), будем знакомы + - В этой беседе моё имя [Светлана](person_name) + - Для знакомства, я [Бахтиёр](person_name) + - [Анна-Мария](person_name) — так звучит моё имя + - Будем знакомы, перед тобой [Али-Акбар](person_name) + - По документам я [Саида Каримова](person_name) + - Я представлюсь полностью: [Отабек Юсупов](person_name) + - Пришло время познакомиться: я [Нодира Усманова](person_name) + - На обращение [Темур-Бек](person_name) я откликнусь + - Сохрани моё имя: [Александр Сергеев](person_name) + + - intent: ask_restart + examples: | + - Сбрось текущую беседу и открой новую + - Очисти контекст разговора + - Давай с чистого листа + - Забудь предыдущие сообщения + - Перезапусти нашу беседу + - Хочу начать новую сессию + - Сбрось историю текущего диалога + - Верни разговор в начальное состояние + - Обнули контекст, пожалуйста + - Можно всё начать сначала? + + - intent: ask_rasa + examples: | + - Объясни, какую технологию называют Rasa + - Для чего нужна платформа Rasa? + - Какую роль Rasa играет в этом боте? + - Объясни назначение Rasa + - Rasa — это библиотека или сервис? + - Зачем проекту используется Rasa? + - Что делает фреймворк Rasa? + - Расскажи, как Rasa участвует в обработке сообщений + - На чём основана Rasa? + - Какие задачи в системе выполняет Rasa? + + - intent: ask_nlu + examples: | + - Что такое NLU? + - Как бот понимает смысл текста? + - Объясни разницу между интентом и сущностью + - Как определяется намерение пользователя? + - Что называют entity в NLU? + - Каким образом из сообщения извлекаются сущности? + - За что отвечает модуль понимания языка? + - Как фраза преобразуется в intent? + - Расскажи про распознавание интентов + - Что происходит на этапе анализа пользовательского текста? + + - intent: ask_training + examples: | + - Как обучить модель с нуля? + - Какие данные нужны для обучения бота? + - Опиши процесс тренировки NLU + - Что происходит до получения готовой модели? + - Как запустить обучение проекта? + - Из каких этапов состоит подготовка модели? + - Где берутся примеры для тренировки? + - Нужно ли переобучать модель после изменения данных? + - Как формируется обученная модель Rasa? + - Расскажи о подготовке датасета и обучении + + - intent: ask_run + examples: | + - Как запустить уже обученную модель? + - Чем проверить готового бота? + - Какая команда запускает Rasa после обучения? + - Как протестировать сохранённую модель? + - Что нужно сделать, чтобы поговорить с обученным ботом? + - Как поднять проект для локальной проверки? + - Где проверить ответы готовой модели? + - Как запустить чат с существующей моделью? + - Можно ли проверить модель без повторного обучения? + - Опиши запуск бота в режиме проверки + + - intent: ask_project_structure + examples: | + - Как устроена структура проекта? + - За что отвечают файлы в репозитории? + - Какие каталоги есть у Rasa-проекта? + - Где находятся настройки и данные? + - Расшифруй роль ключевых файлов проекта + - В какой папке лежат модели? + - Что обычно хранится в каталоге actions? + - Покажи схему файлов проекта + - Где искать конфигурацию Rasa? + - Расскажи, как организованы директории + + - intent: ask_docker + examples: | + - Как запустить проект в Docker? + - Для чего нужен Docker Compose? + - Какие контейнеры используются системой? + - Как сервисы общаются внутри docker-сети? + - Какие порты нужно открыть для контейнеров? + - Где описана конфигурация Compose? + - Как собрать Docker-образ бота? + - Почему контейнер Rasa не видит action server? + - Как проверить запущенные сервисы Docker? + - Объясни контейнерную схему проекта + + - intent: feedback_bad + examples: | + - Это неправильный ответ + - Ты мне не помог + - Ответ не соответствует вопросу + - Нет, это совсем не то + - В сообщённых сведениях есть ошибка + - Ты неправильно меня понял + - Такой ответ бесполезен + - Разобраться не получилось: твой ответ увёл в сторону + - Исправься, ответ ошибочный + - Это не решает мою проблему + + - intent: ask_time + examples: | + - Что показывают часы в эту минуту? + - Сообщи актуальные часы и минуты + - Хочу сверить часы — назови время + - Какое время на часах? + - Можешь назвать точное время? + - Уже поздно или ещё рано? + - Скажи, который час прямо сейчас + - Хочу узнать местное время + - Определи, сейчас утро, день, вечер или ночь + - Покажи время на данный момент + + - intent: ask_date + examples: | + - Какой календарный день наступил сегодня? + - Заполни сегодняшнюю строку календаря: число, месяц и год + - Какой сегодня день? + - Подскажи дату на сегодня + - Какой сейчас год, месяц и день? + - Покажи сегодняшнюю дату + - Что у нас сегодня по календарю? + - Можешь сказать, какое нынче число? + - Какая дата стоит сейчас? + - Уточни сегодняшние месяц и число + + - intent: smalltalk_howareyou + examples: | + - Как у тебя дела? + - Как настроение? + - Ты сегодня в порядке? + - Как жизнь у цифрового помощника? + - С тобой нынче всё ладно? + - Удачно ли проходит твой день? + - Есть ли перемены в твоих делах? + - Ты как? + - Каково твоё виртуальное самочувствие? + - В каком ты сегодня расположении духа? + + - intent: smalltalk_joke + examples: | + - Расскажи анекдот + - Пошути, пожалуйста + - Знаешь смешную историю? + - Хочу улыбнуться — придумай что-нибудь забавное + - Можешь рассказать шутку? + - Хочу услышать что-нибудь забавное + - Разряди обстановку хорошей шуткой + - Есть свежий анекдот? + - Брось остроумную реплику + - Давай короткую шутку + + - intent: out_of_scope + examples: | + - Какая погода завтра в Самарканде? + - Переведи слово «дружба» на японский + - Сколько будет 347 умножить на 29? + - Подбери рецепт плова без мяса + - Кто выиграл вчерашний футбольный матч? + - Закажи мне такси до аэропорта + - Найди дешёвые билеты в Алматы + - Включи музыку для концентрации + - Поставь будильник на шесть утра + - Напомни позвонить врачу вечером + - Какой сейчас курс доллара к суму? + - Расскажи последние мировые новости + - Посоветуй сериал на выходные + - Напиши поздравление с годовщиной свадьбы + - Как вылечить сильную боль в спине? + - Можешь составить договор аренды квартиры? + - Где ближайшая круглосуточная аптека? + - Проверь статус посылки по номеру отслеживания + - Проложи маршрут до железнодорожного вокзала + - Подбери смартфон дешевле трёх миллионов сумов + - Объясни квантовую запутанность простыми словами + - Кто написал роман «Преступление и наказание»? + - Как завести автомобиль после морозной ночи? + - Перезапусти мой ноутбук + - Подскажи схему вязания тёплого шарфа спицами + - Как устроена файловая система Windows? + - Что означает слово «раса» в биологии? + - Как меня зовут? + - Сколько времени варить рис? + - Какова дата рождения Александра Пушкина? diff --git a/tests/static_validate.py b/tests/static_validate.py new file mode 100644 index 0000000..fb4b4ba --- /dev/null +++ b/tests/static_validate.py @@ -0,0 +1,806 @@ +"""Static consistency checks for the Russian Rasa assistant. + +The validator checks the project schema, the independent train/test NLU +corpora, dialogue references, custom actions, and pinned container versions. +It intentionally does not replace ``rasa data validate`` or trained-model +evaluation. +""" + +from __future__ import annotations + +import ast +import json +import re +import sys +import unicodedata +from collections import Counter, defaultdict +from difflib import SequenceMatcher +from itertools import combinations +from pathlib import Path +from typing import Any, Iterable, Mapping, Sequence + +try: + import yaml +except ModuleNotFoundError: + print("STATIC VALIDATION: FAILED") + print(" - PyYAML is not installed; install the dependencies from requirements.txt") + raise SystemExit(2) + + +ROOT = Path(__file__).resolve().parents[1] + +EXPECTED_INTENTS = { + "greet", + "goodbye", + "thanks", + "help", + "ask_name", + "ask_creator", + "ask_project", + "inform_name", + "ask_restart", + "ask_rasa", + "ask_nlu", + "ask_training", + "ask_run", + "ask_project_structure", + "ask_docker", + "feedback_bad", + "ask_time", + "ask_date", + "smalltalk_howareyou", + "smalltalk_joke", + "out_of_scope", +} + +SYSTEM_INTENTS = {"nlu_fallback"} + +REQUIRED_FILES = ( + "requirements.txt", + "config.yml", + "domain.yml", + "endpoints.yml", + "docker-compose.yml", + "actions/__init__.py", + "actions/actions.py", + "data/nlu.yml", + "data/rules.yml", + "data/stories.yml", + "tests/nlu_dev.yml", + "tests/nlu_test.yml", + "tests/test_stories.yml", + "README.md", + ".gitignore", +) + +SCHEMA_31_FILES = ( + "domain.yml", + "data/nlu.yml", + "data/rules.yml", + "data/stories.yml", + "tests/nlu_dev.yml", + "tests/nlu_test.yml", + "tests/test_stories.yml", +) + +BUILT_IN_ACTIONS = { + "action_back", + "action_deactivate_loop", + "action_default_ask_affirmation", + "action_default_ask_rephrase", + "action_default_fallback", + "action_extract_slots", + "action_listen", + "action_restart", + "action_revert_fallback_events", + "action_session_start", + "action_two_stage_fallback", + "action_unlikely_intent", +} + +INLINE_ENTITY_PATTERN = re.compile( + r"\[(?P[^\]]+)\]\((?P[A-Za-z_][A-Za-z0-9_.-]*)\)" +) +JSON_ENTITY_PATTERN = re.compile(r"\[(?P[^\]]+)\](?P\{[^{}]+\})") + + +class ValidationContext: + def __init__(self) -> None: + self.errors: list[str] = [] + self.documents: dict[str, Any] = {} + + def error(self, message: str) -> None: + self.errors.append(message) + + def load_yaml(self, relative_path: str) -> Any: + path = ROOT / relative_path + if relative_path in self.documents: + return self.documents[relative_path] + if not path.is_file(): + return None + try: + with path.open("r", encoding="utf-8") as source: + document = yaml.safe_load(source) + except (OSError, UnicodeError, yaml.YAMLError) as exc: + self.error(f"{relative_path}: invalid YAML: {exc}") + document = None + self.documents[relative_path] = document + return document + + +def normalize_text(text: str) -> str: + """Normalize examples for duplicate and leakage checks.""" + + text = INLINE_ENTITY_PATTERN.sub(lambda match: match.group("value"), text) + text = JSON_ENTITY_PATTERN.sub(lambda match: match.group("value"), text) + text = unicodedata.normalize("NFKC", text).casefold().replace("ё", "е") + return " ".join(re.findall(r"\w+", text, flags=re.UNICODE)) + + +def normalize_name(text: str) -> str: + return normalize_text(text) + + +def normalize_entity_template(text: str) -> str: + """Normalize wording while masking entity values for leakage checks.""" + + text = INLINE_ENTITY_PATTERN.sub("__entity__", text) + text = JSON_ENTITY_PATTERN.sub("__entity__", text) + text = unicodedata.normalize("NFKC", text).casefold().replace("ё", "е") + return " ".join(re.findall(r"\w+", text, flags=re.UNICODE)) + + +def string_set(values: Any, field: str, context: ValidationContext) -> set[str]: + if values is None: + return set() + if not isinstance(values, list): + context.error(f"{field}: expected a list") + return set() + + result: set[str] = set() + for value in values: + if isinstance(value, str): + result.add(value) + elif isinstance(value, Mapping) and len(value) == 1: + key = next(iter(value)) + if isinstance(key, str): + result.add(key) + else: + context.error(f"{field}: mapping key must be a string: {value!r}") + else: + context.error(f"{field}: unsupported list item: {value!r}") + return result + + +def parse_example_block( + raw_examples: Any, + location: str, + context: ValidationContext, +) -> list[str]: + if isinstance(raw_examples, str): + result: list[str] = [] + for line_number, line in enumerate(raw_examples.splitlines(), start=1): + stripped = line.strip() + if not stripped: + continue + if not stripped.startswith("-"): + context.error( + f"{location}: example line {line_number} must start with '-': {line!r}" + ) + continue + example = stripped[1:].strip() + if not example: + context.error(f"{location}: empty example on line {line_number}") + continue + result.append(example) + return result + + if isinstance(raw_examples, list): + result = [] + for index, value in enumerate(raw_examples, start=1): + if not isinstance(value, str) or not value.strip(): + context.error(f"{location}: invalid example #{index}: {value!r}") + continue + result.append(value.strip()) + return result + + context.error(f"{location}: examples must be a block string or a list") + return [] + + +def nlu_examples( + document: Any, + relative_path: str, + context: ValidationContext, +) -> dict[str, list[str]]: + if not isinstance(document, Mapping): + context.error(f"{relative_path}: top-level YAML value must be a mapping") + return {} + items = document.get("nlu") + if not isinstance(items, list): + context.error(f"{relative_path}: top-level 'nlu' must be a list") + return {} + + result: dict[str, list[str]] = {} + block_counts: Counter[str] = Counter() + for index, item in enumerate(items, start=1): + if not isinstance(item, Mapping): + context.error(f"{relative_path}: nlu item #{index} must be a mapping") + continue + intent = item.get("intent") + if intent is None: + # Rasa also allows regex, lookup and synonym entries in this list. + continue + if not isinstance(intent, str) or not intent: + context.error(f"{relative_path}: nlu item #{index} has an invalid intent") + continue + block_counts[intent] += 1 + result.setdefault(intent, []).extend( + parse_example_block( + item.get("examples"), + f"{relative_path}:{intent}", + context, + ) + ) + + repeated = sorted(intent for intent, count in block_counts.items() if count > 1) + if repeated: + context.error( + f"{relative_path}: intents are split across repeated blocks: " + + ", ".join(repeated) + ) + return result + + +def duplicate_examples( + examples_by_intent: Mapping[str, Sequence[str]], +) -> dict[str, list[tuple[str, str]]]: + grouped: defaultdict[str, list[tuple[str, str]]] = defaultdict(list) + for intent, examples in examples_by_intent.items(): + for example in examples: + grouped[normalize_text(example)].append((intent, example)) + return { + normalized: occurrences + for normalized, occurrences in grouped.items() + if normalized and len(occurrences) > 1 + } + + +def entity_annotations(text: str, location: str, context: ValidationContext) -> list[tuple[str, str]]: + annotations = [ + (match.group("value"), match.group("entity")) + for match in INLINE_ENTITY_PATTERN.finditer(text) + ] + for match in JSON_ENTITY_PATTERN.finditer(text): + try: + metadata = json.loads(match.group("meta")) + except json.JSONDecodeError as exc: + context.error(f"{location}: invalid JSON entity annotation: {exc}") + continue + entity = metadata.get("entity") if isinstance(metadata, Mapping) else None + if not isinstance(entity, str): + context.error(f"{location}: JSON entity annotation has no string 'entity'") + continue + annotations.append((match.group("value"), entity)) + return annotations + + +def validate_entities( + datasets: Mapping[str, Mapping[str, Sequence[str]]], + declared_entities: set[str], + context: ValidationContext, +) -> tuple[dict[str, int], dict[str, int]]: + names_by_split: dict[str, set[str]] = { + split: set() for split in datasets + } + counts: dict[str, int] = {split: 0 for split in datasets} + + for split, dataset in datasets.items(): + name_values = names_by_split[split] + for intent, examples in dataset.items(): + for index, example in enumerate(examples, start=1): + location = f"{split}:{intent} example #{index}" + annotations = entity_annotations(example, location, context) + for value, entity in annotations: + if entity not in declared_entities: + context.error(f"{location}: undeclared entity '{entity}'") + if entity == "person_name": + counts[split] += 1 + name_values.add(normalize_name(value)) + if intent != "inform_name": + context.error( + f"{location}: person_name may only be annotated in inform_name" + ) + + person_annotations = [ + value for value, entity in annotations if entity == "person_name" + ] + if intent == "inform_name" and len(person_annotations) != 1: + context.error( + f"{location}: expected exactly one person_name annotation, " + f"found {len(person_annotations)}" + ) + + for left, right in combinations(names_by_split, 2): + leaked_names = sorted(names_by_split[left] & names_by_split[right]) + if leaked_names: + context.error( + f"person_name values overlap between {left} and {right}: " + + ", ".join(leaked_names) + ) + + train_names = names_by_split.get("train", set()) + unseen_from_train = { + split: len(names - train_names) + for split, names in names_by_split.items() + if split != "train" + } + return counts, unseen_from_train + + +def maximum_same_intent_similarity( + train: Mapping[str, Sequence[str]], + test: Mapping[str, Sequence[str]], +) -> tuple[float, str | None, int | None]: + """Return the largest train/test similarity after masking entity values.""" + + maximum = 0.0 + maximum_intent: str | None = None + maximum_index: int | None = None + for intent, test_examples in test.items(): + train_templates = [ + normalize_entity_template(example) + for example in train.get(intent, ()) + ] + for index, example in enumerate(test_examples, start=1): + template = normalize_entity_template(example) + score = max( + ( + SequenceMatcher(None, template, candidate).ratio() + for candidate in train_templates + ), + default=0.0, + ) + if score > maximum: + maximum = score + maximum_intent = intent + maximum_index = index + return maximum, maximum_intent, maximum_index + + +def walk_key_values(value: Any, key: str) -> Iterable[Any]: + if isinstance(value, Mapping): + for candidate_key, candidate_value in value.items(): + if candidate_key == key: + yield candidate_value + yield from walk_key_values(candidate_value, key) + elif isinstance(value, list): + for item in value: + yield from walk_key_values(item, key) + + +def collect_string_references(value: Any, key: str) -> set[str]: + return { + reference + for reference in walk_key_values(value, key) + if isinstance(reference, str) + } + + +def action_implementation_details(source: str) -> tuple[set[str], set[str]]: + tree = ast.parse(source, filename="actions/actions.py") + implemented: set[str] = set() + response_references: set[str] = set() + + for class_node in (node for node in tree.body if isinstance(node, ast.ClassDef)): + inherits_action = any( + isinstance(base, ast.Name) and base.id == "Action" + or isinstance(base, ast.Attribute) and base.attr == "Action" + for base in class_node.bases + ) + if not inherits_action: + continue + for function_node in ( + node + for node in class_node.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name == "name" + ): + for return_node in ast.walk(function_node): + if ( + isinstance(return_node, ast.Return) + and isinstance(return_node.value, ast.Constant) + and isinstance(return_node.value.value, str) + ): + implemented.add(return_node.value.value) + + for call in (node for node in ast.walk(tree) if isinstance(node, ast.Call)): + for keyword in call.keywords: + if keyword.arg not in {"response", "template"}: + continue + if isinstance(keyword.value, ast.Constant) and isinstance( + keyword.value.value, str + ): + response_references.add(keyword.value.value) + + return implemented, response_references + + +def schema_version(document: Any) -> str | None: + if not isinstance(document, Mapping): + return None + value = document.get("version") + return str(value) if value is not None else None + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + + context = ValidationContext() + for relative_path in REQUIRED_FILES: + if not (ROOT / relative_path).is_file(): + context.error(f"missing required file: {relative_path}") + + for relative_path in SCHEMA_31_FILES: + document = context.load_yaml(relative_path) + if document is not None and schema_version(document) != "3.1": + context.error(f"{relative_path}: expected schema version 3.1") + + domain = context.load_yaml("domain.yml") + train_document = context.load_yaml("data/nlu.yml") + dev_document = context.load_yaml("tests/nlu_dev.yml") + test_document = context.load_yaml("tests/nlu_test.yml") + rules = context.load_yaml("data/rules.yml") + stories = context.load_yaml("data/stories.yml") + test_stories = context.load_yaml("tests/test_stories.yml") + compose = context.load_yaml("docker-compose.yml") + + train = nlu_examples(train_document, "data/nlu.yml", context) + dev = nlu_examples(dev_document, "tests/nlu_dev.yml", context) + test = nlu_examples(test_document, "tests/nlu_test.yml", context) + train_intents = set(train) + dev_intents = set(dev) + test_intents = set(test) + + for split, split_intents in (("dev", dev_intents), ("test", test_intents)): + if train_intents != split_intents: + context.error( + f"train/{split} intent sets differ; only in train: " + + ", ".join(sorted(train_intents - split_intents)) + + f"; only in {split}: " + + ", ".join(sorted(split_intents - train_intents)) + ) + if train_intents != EXPECTED_INTENTS: + context.error( + "train intent taxonomy differs from the expected 21 intents; missing: " + + ", ".join(sorted(EXPECTED_INTENTS - train_intents)) + + "; unexpected: " + + ", ".join(sorted(train_intents - EXPECTED_INTENTS)) + ) + + ordinary_intents = EXPECTED_INTENTS - {"inform_name", "out_of_scope"} + expected_train_counts = { + intent: 35 for intent in ordinary_intents + } | {"inform_name": 60, "out_of_scope": 70} + expected_eval_counts = { + intent: 10 for intent in ordinary_intents + } | {"inform_name": 15, "out_of_scope": 30} + + for intent, expected in sorted(expected_train_counts.items()): + actual = len(train.get(intent, ())) + if actual != expected: + context.error( + f"data/nlu.yml:{intent}: expected exactly {expected} train examples, " + f"found {actual}" + ) + for split, dataset in (("dev", dev), ("test", test)): + for intent, expected in sorted(expected_eval_counts.items()): + actual = len(dataset.get(intent, ())) + if actual != expected: + context.error( + f"tests/nlu_{split}.yml:{intent}: expected exactly {expected} " + f"examples, found {actual}" + ) + + train_duplicates = duplicate_examples(train) + dev_duplicates = duplicate_examples(dev) + test_duplicates = duplicate_examples(test) + if train_duplicates: + context.error( + f"data/nlu.yml: found {len(train_duplicates)} normalized duplicate group(s)" + ) + if dev_duplicates: + context.error( + f"tests/nlu_dev.yml: found {len(dev_duplicates)} normalized duplicate group(s)" + ) + if test_duplicates: + context.error( + f"tests/nlu_test.yml: found {len(test_duplicates)} normalized duplicate group(s)" + ) + + train_texts = { + normalize_text(example) + for examples in train.values() + for example in examples + } + dev_texts = { + normalize_text(example) + for examples in dev.values() + for example in examples + } + test_texts = { + normalize_text(example) + for examples in test.values() + for example in examples + } + leakage_by_pair = { + "train/dev": sorted((train_texts & dev_texts) - {""}), + "train/test": sorted((train_texts & test_texts) - {""}), + "dev/test": sorted((dev_texts & test_texts) - {""}), + } + for pair, leakage in leakage_by_pair.items(): + if leakage: + context.error( + f"{pair} exact leakage: found {len(leakage)} normalized example(s)" + ) + + train_templates = { + normalize_entity_template(example) + for examples in train.values() + for example in examples + } + test_templates = [ + normalize_entity_template(example) + for examples in test.values() + for example in examples + ] + repeated_test_templates = len(test_templates) - len(set(test_templates)) + if repeated_test_templates: + context.error( + "tests/nlu_test.yml: entity-masked templates contain " + f"{repeated_test_templates} duplicate(s)" + ) + template_leakage = (train_templates & set(test_templates)) - {""} + if template_leakage: + context.error( + "train/test entity-template leakage: found " + f"{len(template_leakage)} normalized template(s)" + ) + + maximum_similarity, similar_intent, similar_index = ( + maximum_same_intent_similarity(train, test) + ) + if maximum_similarity >= 0.85: + context.error( + "train/test near-copy threshold exceeded: " + f"{similar_intent} example #{similar_index} has similarity " + f"{maximum_similarity:.3f} (limit < 0.850)" + ) + + dev_test_maximum_similarity, dev_test_similar_intent, dev_test_similar_index = ( + maximum_same_intent_similarity(dev, test) + ) + if dev_test_maximum_similarity >= 0.85: + context.error( + "dev/test near-copy threshold exceeded: " + f"{dev_test_similar_intent} example #{dev_test_similar_index} has " + f"similarity {dev_test_maximum_similarity:.3f} (limit < 0.850)" + ) + + domain_mapping = domain if isinstance(domain, Mapping) else {} + domain_intents = string_set(domain_mapping.get("intents"), "domain.yml:intents", context) + expected_domain_intents = train_intents | SYSTEM_INTENTS + if domain_intents != expected_domain_intents: + context.error( + "domain intents must equal train intents plus nlu_fallback; missing: " + + ", ".join(sorted(expected_domain_intents - domain_intents)) + + "; unexpected: " + + ", ".join(sorted(domain_intents - expected_domain_intents)) + ) + + declared_entities = string_set( + domain_mapping.get("entities"), "domain.yml:entities", context + ) + if "person_name" not in declared_entities: + context.error("domain.yml: entity person_name is not declared") + + slots = domain_mapping.get("slots") + user_name = slots.get("user_name") if isinstance(slots, Mapping) else None + mappings = user_name.get("mappings") if isinstance(user_name, Mapping) else None + if not ( + isinstance(mappings, list) + and any( + isinstance(mapping, Mapping) + and mapping.get("type") == "from_entity" + and mapping.get("entity") == "person_name" + for mapping in mappings + ) + ): + context.error("domain.yml: slot user_name is not mapped from person_name") + + entity_counts, unseen_names = validate_entities( + {"train": train, "dev": dev, "test": test}, + declared_entities, + context, + ) + + responses_value = domain_mapping.get("responses") + responses = set(responses_value) if isinstance(responses_value, Mapping) else set() + if not isinstance(responses_value, Mapping): + context.error("domain.yml: responses must be a mapping") + else: + for response_name, variants in responses_value.items(): + if not isinstance(variants, list) or not variants: + context.error(f"domain.yml:{response_name}: response variants are empty") + + declared_actions = string_set( + domain_mapping.get("actions"), "domain.yml:actions", context + ) + + dialogue_documents = { + "data/rules.yml": rules, + "data/stories.yml": stories, + "tests/test_stories.yml": test_stories, + } + intent_references: set[str] = set() + action_references: set[str] = set() + response_references: set[str] = set() + for relative_path, document in dialogue_documents.items(): + if not isinstance(document, Mapping): + context.error(f"{relative_path}: top-level YAML value must be a mapping") + continue + file_intents = collect_string_references(document, "intent") + file_actions = collect_string_references(document, "action") + file_responses = collect_string_references(document, "response") + intent_references.update(file_intents) + action_references.update(file_actions) + response_references.update(file_responses) + + unknown_intents = file_intents - domain_intents + if unknown_intents: + context.error( + f"{relative_path}: unknown intent reference(s): " + + ", ".join(sorted(unknown_intents)) + ) + + response_references.update( + action for action in action_references if action.startswith("utter_") + ) + missing_responses = response_references - responses + if missing_responses: + context.error( + "dialogue/action response reference(s) missing from domain: " + + ", ".join(sorted(missing_responses)) + ) + + custom_action_references = { + action + for action in action_references + if not action.startswith("utter_") and action not in BUILT_IN_ACTIONS + } + undeclared_action_references = custom_action_references - declared_actions + if undeclared_action_references: + context.error( + "dialogue custom action reference(s) missing from domain: " + + ", ".join(sorted(undeclared_action_references)) + ) + + action_source_path = ROOT / "actions/actions.py" + implemented_actions: set[str] = set() + source_response_references: set[str] = set() + if action_source_path.is_file(): + try: + source = action_source_path.read_text(encoding="utf-8") + implemented_actions, source_response_references = action_implementation_details( + source + ) + except (OSError, UnicodeError, SyntaxError) as exc: + context.error(f"actions/actions.py: cannot inspect custom actions: {exc}") + + missing_implementations = declared_actions - implemented_actions + undeclared_implementations = implemented_actions - declared_actions + if missing_implementations: + context.error( + "declared custom action(s) without implementation: " + + ", ".join(sorted(missing_implementations)) + ) + if undeclared_implementations: + context.error( + "implemented custom action(s) missing from domain: " + + ", ".join(sorted(undeclared_implementations)) + ) + unused_actions = declared_actions - custom_action_references + if unused_actions: + context.error( + "declared custom action(s) are not referenced by dialogue data: " + + ", ".join(sorted(unused_actions)) + ) + missing_source_responses = source_response_references - responses + if missing_source_responses: + context.error( + "actions/actions.py references unknown response(s): " + + ", ".join(sorted(missing_source_responses)) + ) + + services = compose.get("services") if isinstance(compose, Mapping) else None + if not isinstance(services, Mapping): + context.error("docker-compose.yml: top-level services mapping is missing") + else: + rasa_service = services.get("rasa") + action_service = services.get("action_server") + rasa_image = rasa_service.get("image") if isinstance(rasa_service, Mapping) else None + action_image = ( + action_service.get("image") if isinstance(action_service, Mapping) else None + ) + if rasa_image != "rasa/rasa:3.6.21": + context.error( + "docker-compose.yml: rasa image must be pinned to rasa/rasa:3.6.21" + ) + if action_image != "rasa/rasa-sdk:3.6.2": + context.error( + "docker-compose.yml: action server image must be pinned to " + "rasa/rasa-sdk:3.6.2" + ) + + metrics = { + "train_intents": len(train_intents), + "dev_intents": len(dev_intents), + "test_intents": len(test_intents), + "train_examples": sum(len(examples) for examples in train.values()), + "dev_examples": sum(len(examples) for examples in dev.values()), + "test_examples": sum(len(examples) for examples in test.values()), + "train_person_name_annotations": entity_counts.get("train", 0), + "dev_person_name_annotations": entity_counts.get("dev", 0), + "test_person_name_annotations": entity_counts.get("test", 0), + "unseen_dev_person_names": unseen_names.get("dev", 0), + "unseen_test_person_names": unseen_names.get("test", 0), + "normalized_train_duplicates": len(train_duplicates), + "normalized_dev_duplicates": len(dev_duplicates), + "normalized_test_duplicates": len(test_duplicates), + "train_dev_exact_overlap": len(leakage_by_pair["train/dev"]), + "train_test_exact_overlap": len(leakage_by_pair["train/test"]), + "dev_test_exact_overlap": len(leakage_by_pair["dev/test"]), + "train_test_template_overlap": len(template_leakage), + "train_test_max_same_intent_similarity": f"{maximum_similarity:.3f}", + "dev_test_max_same_intent_similarity": ( + f"{dev_test_maximum_similarity:.3f}" + ), + "domain_responses": len(responses), + "declared_custom_actions": len(declared_actions), + "implemented_custom_actions": len(implemented_actions), + "rules": len(rules.get("rules", [])) if isinstance(rules, Mapping) else 0, + "stories": len(stories.get("stories", [])) if isinstance(stories, Mapping) else 0, + "test_stories": ( + len(test_stories.get("stories", [])) + if isinstance(test_stories, Mapping) + else 0 + ), + } + + print("Static project metrics:") + for key, value in metrics.items(): + print(f" {key}: {value}") + print("Per-intent train/dev/test examples:") + for intent in sorted(train_intents | dev_intents | test_intents): + print( + f" {intent}: {len(train.get(intent, ()))} / " + f"{len(dev.get(intent, ()))} / {len(test.get(intent, ()))}" + ) + + if context.errors: + print("\nSTATIC VALIDATION: FAILED") + for error in context.errors: + print(f" - {error}") + return 1 + + print("\nSTATIC VALIDATION: OK") + print( + "Static consistency passed. Run Rasa validation and trained-model tests " + "for runtime and quality metrics." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_actions.py b/tests/test_actions.py new file mode 100644 index 0000000..823f6f3 --- /dev/null +++ b/tests/test_actions.py @@ -0,0 +1,140 @@ +from datetime import datetime, timedelta +from typing import Any, Optional + +import pytest +from rasa_sdk import Tracker +from rasa_sdk.executor import CollectingDispatcher + +from actions import actions as actions_module +from actions.actions import ( + ActionRememberName, + ActionShowCapabilities, + ActionShowDate, + ActionShowTime, +) + + +def make_tracker(user_name: Optional[str] = None) -> Tracker: + """Build the smallest real Rasa tracker needed by the custom actions.""" + return Tracker( + sender_id="unit-test-user", + slots={"user_name": user_name}, + latest_message={}, + events=[], + paused=False, + followup_action=None, + active_loop={}, + latest_action_name=None, + ) + + +def run_action(action: Any, user_name: Optional[str] = None) -> tuple[list, str]: + dispatcher = CollectingDispatcher() + + events = action.run(dispatcher, make_tracker(user_name), {}) + + assert len(dispatcher.messages) == 1 + text = dispatcher.messages[0].get("text") + assert isinstance(text, str) and text + assert any("а" <= character.lower() <= "я" or character.lower() == "ё" for character in text) + assert "Ð" not in text and "Ñ" not in text and "�" not in text + return events, text + + +@pytest.mark.parametrize( + ("action", "expected_name"), + [ + (ActionRememberName(), "action_remember_name"), + (ActionShowCapabilities(), "action_show_capabilities"), + (ActionShowTime(), "action_show_time"), + (ActionShowDate(), "action_show_date"), + ], +) +def test_action_names(action: Any, expected_name: str) -> None: + assert action.name() == expected_name + + +def test_remember_name_addresses_user_and_returns_no_events() -> None: + events, text = run_action(ActionRememberName(), "Анна") + + assert events == [] + assert text == ( + "Приятно познакомиться, Анна! " + "Я запомню ваше имя в рамках текущего диалога." + ) + + +@pytest.mark.parametrize("missing_name", [None, ""]) +def test_remember_name_asks_to_repeat_when_slot_is_empty( + missing_name: Optional[str], +) -> None: + events, text = run_action(ActionRememberName(), missing_name) + + assert events == [] + assert text == ( + "Я не смог выделить имя. " + "Напишите, например: «Меня зовут Анна»." + ) + + +@pytest.mark.parametrize( + ("user_name", "expected_prefix"), + [(None, "Я могу"), ("Илья", "Илья, я могу")], +) +def test_show_capabilities_lists_supported_topics( + user_name: Optional[str], expected_prefix: str +) -> None: + events, text = run_action(ActionShowCapabilities(), user_name) + + assert events == [] + assert text.startswith(expected_prefix) + for topic in ( + "Rasa", + "NLU", + "Docker", + "обучение", + "запуск", + "структуру файлов", + "дату и время", + ): + assert topic in text + + +@pytest.fixture +def fixed_datetime(monkeypatch: pytest.MonkeyPatch) -> list: + requested_timezones = [] + + class FixedDateTime(datetime): + @classmethod + def now(cls, tz=None): + requested_timezones.append(tz) + return cls(2026, 7, 22, 9, 5, 0, tzinfo=tz) + + monkeypatch.setattr(actions_module, "datetime", FixedDateTime) + return requested_timezones + + +def assert_tashkent_timezone_was_requested(requested_timezones: list) -> None: + assert len(requested_timezones) == 1 + assert requested_timezones[0] is not None + assert requested_timezones[0].utcoffset(None) == timedelta(hours=5) + + +def test_show_time_uses_tashkent_timezone_and_zero_padded_format( + fixed_datetime: list, +) -> None: + events, text = run_action(ActionShowTime()) + + assert events == [] + assert text == "Сейчас 09:05 по времени Ташкента (UTC+5)." + assert_tashkent_timezone_was_requested(fixed_datetime) + + +def test_show_date_uses_russian_month_and_tashkent_timezone( + fixed_datetime: list, +) -> None: + events, text = run_action(ActionShowDate()) + + assert events == [] + assert text == "Сегодня 22 июля 2026 года по времени Ташкента." + assert_tashkent_timezone_was_requested(fixed_datetime) diff --git a/tests/test_stories.yml b/tests/test_stories.yml new file mode 100644 index 0000000..fd3c946 --- /dev/null +++ b/tests/test_stories.yml @@ -0,0 +1,83 @@ +version: "3.1" + +# Эти сценарии проверяют только предсказание действий Rasa Core. +# Качество распознавания текста проверяется отдельно на tests/nlu_test.yml. +stories: +- story: Core — знакомство и обзор проекта + steps: + - intent: greet + - action: utter_greet + - intent: inform_name + entities: + - person_name: Анна + - action: action_remember_name + - intent: help + - action: action_show_capabilities + - intent: ask_project + - action: utter_project + +- story: Core — имя и автор + steps: + - intent: ask_name + - action: utter_name + - intent: ask_creator + - action: utter_creator + - intent: thanks + - action: utter_you_are_welcome + +- story: Core — Rasa NLU и обучение + steps: + - intent: ask_rasa + - action: utter_rasa + - intent: ask_nlu + - action: utter_nlu + - intent: ask_training + - action: utter_training + +- story: Core — структура Docker и запуск + steps: + - intent: ask_project_structure + - action: utter_project_structure + - intent: ask_docker + - action: utter_docker + - intent: ask_run + - action: utter_run + +- story: Core — дата и время + steps: + - intent: ask_date + - action: action_show_date + - intent: ask_time + - action: action_show_time + +- story: Core — короткий разговор + steps: + - intent: smalltalk_howareyou + - action: utter_howareyou + - intent: smalltalk_joke + - action: utter_joke + - intent: goodbye + - action: utter_goodbye + +- story: Core — помощь после плохого ответа + steps: + - intent: feedback_bad + - action: utter_feedback_bad + - intent: help + - action: action_show_capabilities + +- story: Core — известная неподдерживаемая тема + steps: + - intent: out_of_scope + - action: utter_out_of_scope + +- story: Core — низкая уверенность NLU + steps: + - intent: nlu_fallback + - action: utter_default + +- story: Core — перезапуск разговора + steps: + - intent: ask_restart + - action: utter_restart + - action: action_restart