rasa-pro 3.12.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rasa-pro might be problematic. Click here for more details.
- README.md +41 -0
- rasa/__init__.py +9 -0
- rasa/__main__.py +177 -0
- rasa/anonymization/__init__.py +2 -0
- rasa/anonymization/anonymisation_rule_yaml_reader.py +91 -0
- rasa/anonymization/anonymization_pipeline.py +286 -0
- rasa/anonymization/anonymization_rule_executor.py +260 -0
- rasa/anonymization/anonymization_rule_orchestrator.py +120 -0
- rasa/anonymization/schemas/config.yml +47 -0
- rasa/anonymization/utils.py +118 -0
- rasa/api.py +160 -0
- rasa/cli/__init__.py +5 -0
- rasa/cli/arguments/__init__.py +0 -0
- rasa/cli/arguments/data.py +106 -0
- rasa/cli/arguments/default_arguments.py +207 -0
- rasa/cli/arguments/evaluate.py +65 -0
- rasa/cli/arguments/export.py +51 -0
- rasa/cli/arguments/interactive.py +74 -0
- rasa/cli/arguments/run.py +219 -0
- rasa/cli/arguments/shell.py +17 -0
- rasa/cli/arguments/test.py +211 -0
- rasa/cli/arguments/train.py +279 -0
- rasa/cli/arguments/visualize.py +34 -0
- rasa/cli/arguments/x.py +30 -0
- rasa/cli/data.py +354 -0
- rasa/cli/dialogue_understanding_test.py +251 -0
- rasa/cli/e2e_test.py +259 -0
- rasa/cli/evaluate.py +222 -0
- rasa/cli/export.py +250 -0
- rasa/cli/inspect.py +75 -0
- rasa/cli/interactive.py +166 -0
- rasa/cli/license.py +65 -0
- rasa/cli/llm_fine_tuning.py +403 -0
- rasa/cli/markers.py +78 -0
- rasa/cli/project_templates/__init__.py +0 -0
- rasa/cli/project_templates/calm/actions/__init__.py +0 -0
- rasa/cli/project_templates/calm/actions/action_template.py +27 -0
- rasa/cli/project_templates/calm/actions/add_contact.py +30 -0
- rasa/cli/project_templates/calm/actions/db.py +57 -0
- rasa/cli/project_templates/calm/actions/list_contacts.py +22 -0
- rasa/cli/project_templates/calm/actions/remove_contact.py +35 -0
- rasa/cli/project_templates/calm/config.yml +10 -0
- rasa/cli/project_templates/calm/credentials.yml +33 -0
- rasa/cli/project_templates/calm/data/flows/add_contact.yml +31 -0
- rasa/cli/project_templates/calm/data/flows/list_contacts.yml +14 -0
- rasa/cli/project_templates/calm/data/flows/remove_contact.yml +29 -0
- rasa/cli/project_templates/calm/db/contacts.json +10 -0
- rasa/cli/project_templates/calm/domain/add_contact.yml +39 -0
- rasa/cli/project_templates/calm/domain/list_contacts.yml +17 -0
- rasa/cli/project_templates/calm/domain/remove_contact.yml +38 -0
- rasa/cli/project_templates/calm/domain/shared.yml +10 -0
- rasa/cli/project_templates/calm/e2e_tests/cancelations/user_cancels_during_a_correction.yml +16 -0
- rasa/cli/project_templates/calm/e2e_tests/cancelations/user_changes_mind_on_a_whim.yml +7 -0
- rasa/cli/project_templates/calm/e2e_tests/corrections/user_corrects_contact_handle.yml +20 -0
- rasa/cli/project_templates/calm/e2e_tests/corrections/user_corrects_contact_name.yml +19 -0
- rasa/cli/project_templates/calm/e2e_tests/happy_paths/user_adds_contact_to_their_list.yml +15 -0
- rasa/cli/project_templates/calm/e2e_tests/happy_paths/user_lists_contacts.yml +5 -0
- rasa/cli/project_templates/calm/e2e_tests/happy_paths/user_removes_contact.yml +11 -0
- rasa/cli/project_templates/calm/e2e_tests/happy_paths/user_removes_contact_from_list.yml +12 -0
- rasa/cli/project_templates/calm/endpoints.yml +58 -0
- rasa/cli/project_templates/default/actions/__init__.py +0 -0
- rasa/cli/project_templates/default/actions/actions.py +27 -0
- rasa/cli/project_templates/default/config.yml +44 -0
- rasa/cli/project_templates/default/credentials.yml +33 -0
- rasa/cli/project_templates/default/data/nlu.yml +91 -0
- rasa/cli/project_templates/default/data/rules.yml +13 -0
- rasa/cli/project_templates/default/data/stories.yml +30 -0
- rasa/cli/project_templates/default/domain.yml +34 -0
- rasa/cli/project_templates/default/endpoints.yml +42 -0
- rasa/cli/project_templates/default/tests/test_stories.yml +91 -0
- rasa/cli/project_templates/tutorial/actions/__init__.py +0 -0
- rasa/cli/project_templates/tutorial/actions/actions.py +22 -0
- rasa/cli/project_templates/tutorial/config.yml +12 -0
- rasa/cli/project_templates/tutorial/credentials.yml +33 -0
- rasa/cli/project_templates/tutorial/data/flows.yml +8 -0
- rasa/cli/project_templates/tutorial/data/patterns.yml +11 -0
- rasa/cli/project_templates/tutorial/domain.yml +35 -0
- rasa/cli/project_templates/tutorial/endpoints.yml +55 -0
- rasa/cli/run.py +143 -0
- rasa/cli/scaffold.py +273 -0
- rasa/cli/shell.py +141 -0
- rasa/cli/studio/__init__.py +0 -0
- rasa/cli/studio/download.py +62 -0
- rasa/cli/studio/studio.py +296 -0
- rasa/cli/studio/train.py +59 -0
- rasa/cli/studio/upload.py +62 -0
- rasa/cli/telemetry.py +102 -0
- rasa/cli/test.py +280 -0
- rasa/cli/train.py +278 -0
- rasa/cli/utils.py +484 -0
- rasa/cli/visualize.py +40 -0
- rasa/cli/x.py +206 -0
- rasa/constants.py +45 -0
- rasa/core/__init__.py +17 -0
- rasa/core/actions/__init__.py +0 -0
- rasa/core/actions/action.py +1318 -0
- rasa/core/actions/action_clean_stack.py +59 -0
- rasa/core/actions/action_exceptions.py +24 -0
- rasa/core/actions/action_hangup.py +29 -0
- rasa/core/actions/action_repeat_bot_messages.py +89 -0
- rasa/core/actions/action_run_slot_rejections.py +210 -0
- rasa/core/actions/action_trigger_chitchat.py +31 -0
- rasa/core/actions/action_trigger_flow.py +109 -0
- rasa/core/actions/action_trigger_search.py +31 -0
- rasa/core/actions/constants.py +5 -0
- rasa/core/actions/custom_action_executor.py +191 -0
- rasa/core/actions/direct_custom_actions_executor.py +109 -0
- rasa/core/actions/e2e_stub_custom_action_executor.py +72 -0
- rasa/core/actions/forms.py +741 -0
- rasa/core/actions/grpc_custom_action_executor.py +251 -0
- rasa/core/actions/http_custom_action_executor.py +145 -0
- rasa/core/actions/loops.py +114 -0
- rasa/core/actions/two_stage_fallback.py +186 -0
- rasa/core/agent.py +559 -0
- rasa/core/auth_retry_tracker_store.py +122 -0
- rasa/core/brokers/__init__.py +0 -0
- rasa/core/brokers/broker.py +126 -0
- rasa/core/brokers/file.py +58 -0
- rasa/core/brokers/kafka.py +324 -0
- rasa/core/brokers/pika.py +388 -0
- rasa/core/brokers/sql.py +86 -0
- rasa/core/channels/__init__.py +61 -0
- rasa/core/channels/botframework.py +338 -0
- rasa/core/channels/callback.py +84 -0
- rasa/core/channels/channel.py +456 -0
- rasa/core/channels/console.py +241 -0
- rasa/core/channels/development_inspector.py +197 -0
- rasa/core/channels/facebook.py +419 -0
- rasa/core/channels/hangouts.py +329 -0
- rasa/core/channels/inspector/.eslintrc.cjs +25 -0
- rasa/core/channels/inspector/.gitignore +23 -0
- rasa/core/channels/inspector/README.md +54 -0
- rasa/core/channels/inspector/assets/favicon.ico +0 -0
- rasa/core/channels/inspector/assets/rasa-chat.js +2 -0
- rasa/core/channels/inspector/custom.d.ts +3 -0
- rasa/core/channels/inspector/dist/assets/arc-861ddd57.js +1 -0
- rasa/core/channels/inspector/dist/assets/array-9f3ba611.js +1 -0
- rasa/core/channels/inspector/dist/assets/c4Diagram-d0fbc5ce-921f02db.js +10 -0
- rasa/core/channels/inspector/dist/assets/classDiagram-936ed81e-b436c4f8.js +2 -0
- rasa/core/channels/inspector/dist/assets/classDiagram-v2-c3cb15f1-511a23cb.js +2 -0
- rasa/core/channels/inspector/dist/assets/createText-62fc7601-ef476ecd.js +7 -0
- rasa/core/channels/inspector/dist/assets/edges-f2ad444c-f1878e0a.js +4 -0
- rasa/core/channels/inspector/dist/assets/erDiagram-9d236eb7-fac75185.js +51 -0
- rasa/core/channels/inspector/dist/assets/flowDb-1972c806-201c5bbc.js +6 -0
- rasa/core/channels/inspector/dist/assets/flowDiagram-7ea5b25a-f904ae41.js +4 -0
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-855bc5b3-b080d6f2.js +1 -0
- rasa/core/channels/inspector/dist/assets/flowchart-elk-definition-abe16c3d-1813da66.js +139 -0
- rasa/core/channels/inspector/dist/assets/ganttDiagram-9b5ea136-872af172.js +266 -0
- rasa/core/channels/inspector/dist/assets/gitGraphDiagram-99d0ae7c-34a0af5a.js +70 -0
- rasa/core/channels/inspector/dist/assets/ibm-plex-mono-v4-latin-regular-128cfa44.ttf +0 -0
- rasa/core/channels/inspector/dist/assets/ibm-plex-mono-v4-latin-regular-21dbcb97.woff +0 -0
- rasa/core/channels/inspector/dist/assets/ibm-plex-mono-v4-latin-regular-222b5e26.svg +329 -0
- rasa/core/channels/inspector/dist/assets/ibm-plex-mono-v4-latin-regular-9ad89b2a.woff2 +0 -0
- rasa/core/channels/inspector/dist/assets/index-2c4b9a3b-42ba3e3d.js +1 -0
- rasa/core/channels/inspector/dist/assets/index-37817b51.js +1317 -0
- rasa/core/channels/inspector/dist/assets/index-3ee28881.css +1 -0
- rasa/core/channels/inspector/dist/assets/infoDiagram-736b4530-6b731386.js +7 -0
- rasa/core/channels/inspector/dist/assets/init-77b53fdd.js +1 -0
- rasa/core/channels/inspector/dist/assets/journeyDiagram-df861f2b-e8579ac6.js +139 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-700-60c05ee4.woff +0 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-700-8335d9b8.svg +438 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-700-9cc39c75.ttf +0 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-700-ead13ccf.woff2 +0 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-regular-16705655.woff2 +0 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-regular-5aeb07f9.woff +0 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-regular-9c459044.ttf +0 -0
- rasa/core/channels/inspector/dist/assets/lato-v14-latin-regular-9e2898a4.svg +435 -0
- rasa/core/channels/inspector/dist/assets/layout-89e6403a.js +1 -0
- rasa/core/channels/inspector/dist/assets/line-dc73d3fc.js +1 -0
- rasa/core/channels/inspector/dist/assets/linear-f5b1d2bc.js +1 -0
- rasa/core/channels/inspector/dist/assets/mindmap-definition-beec6740-82cb74fa.js +109 -0
- rasa/core/channels/inspector/dist/assets/ordinal-ba9b4969.js +1 -0
- rasa/core/channels/inspector/dist/assets/path-53f90ab3.js +1 -0
- rasa/core/channels/inspector/dist/assets/pieDiagram-dbbf0591-bdf5f29b.js +35 -0
- rasa/core/channels/inspector/dist/assets/quadrantDiagram-4d7f4fd6-c7a0cbe4.js +7 -0
- rasa/core/channels/inspector/dist/assets/requirementDiagram-6fc4c22a-7ec5410f.js +52 -0
- rasa/core/channels/inspector/dist/assets/sankeyDiagram-8f13d901-caee5554.js +8 -0
- rasa/core/channels/inspector/dist/assets/sequenceDiagram-b655622a-2935f8db.js +122 -0
- rasa/core/channels/inspector/dist/assets/stateDiagram-59f0c015-8f5d9693.js +1 -0
- rasa/core/channels/inspector/dist/assets/stateDiagram-v2-2b26beab-d565d1de.js +1 -0
- rasa/core/channels/inspector/dist/assets/styles-080da4f6-75ad421d.js +110 -0
- rasa/core/channels/inspector/dist/assets/styles-3dcbcfbf-7e764226.js +159 -0
- rasa/core/channels/inspector/dist/assets/styles-9c745c82-7a4e0e61.js +207 -0
- rasa/core/channels/inspector/dist/assets/svgDrawCommon-4835440b-4019d1bf.js +1 -0
- rasa/core/channels/inspector/dist/assets/timeline-definition-5b62e21b-01ea12df.js +61 -0
- rasa/core/channels/inspector/dist/assets/xychartDiagram-2b33534f-89407137.js +7 -0
- rasa/core/channels/inspector/dist/index.html +42 -0
- rasa/core/channels/inspector/index.html +40 -0
- rasa/core/channels/inspector/jest.config.ts +13 -0
- rasa/core/channels/inspector/package.json +52 -0
- rasa/core/channels/inspector/setupTests.ts +2 -0
- rasa/core/channels/inspector/src/App.tsx +220 -0
- rasa/core/channels/inspector/src/components/Chat.tsx +95 -0
- rasa/core/channels/inspector/src/components/DiagramFlow.tsx +108 -0
- rasa/core/channels/inspector/src/components/DialogueInformation.tsx +187 -0
- rasa/core/channels/inspector/src/components/DialogueStack.tsx +136 -0
- rasa/core/channels/inspector/src/components/ExpandIcon.tsx +16 -0
- rasa/core/channels/inspector/src/components/FullscreenButton.tsx +45 -0
- rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +22 -0
- rasa/core/channels/inspector/src/components/NoActiveFlow.tsx +21 -0
- rasa/core/channels/inspector/src/components/RasaLogo.tsx +32 -0
- rasa/core/channels/inspector/src/components/SaraDiagrams.tsx +39 -0
- rasa/core/channels/inspector/src/components/Slots.tsx +91 -0
- rasa/core/channels/inspector/src/components/Welcome.tsx +54 -0
- rasa/core/channels/inspector/src/helpers/audiostream.ts +191 -0
- rasa/core/channels/inspector/src/helpers/formatters.test.ts +392 -0
- rasa/core/channels/inspector/src/helpers/formatters.ts +306 -0
- rasa/core/channels/inspector/src/helpers/utils.ts +127 -0
- rasa/core/channels/inspector/src/main.tsx +13 -0
- rasa/core/channels/inspector/src/theme/Button/Button.ts +29 -0
- rasa/core/channels/inspector/src/theme/Heading/Heading.ts +31 -0
- rasa/core/channels/inspector/src/theme/Input/Input.ts +27 -0
- rasa/core/channels/inspector/src/theme/Link/Link.ts +10 -0
- rasa/core/channels/inspector/src/theme/Modal/Modal.ts +47 -0
- rasa/core/channels/inspector/src/theme/Table/Table.tsx +38 -0
- rasa/core/channels/inspector/src/theme/Tooltip/Tooltip.ts +12 -0
- rasa/core/channels/inspector/src/theme/base/breakpoints.ts +8 -0
- rasa/core/channels/inspector/src/theme/base/colors.ts +88 -0
- rasa/core/channels/inspector/src/theme/base/fonts/fontFaces.css +29 -0
- rasa/core/channels/inspector/src/theme/base/fonts/ibm-plex-mono-v4-latin/ibm-plex-mono-v4-latin-regular.eot +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/ibm-plex-mono-v4-latin/ibm-plex-mono-v4-latin-regular.svg +329 -0
- rasa/core/channels/inspector/src/theme/base/fonts/ibm-plex-mono-v4-latin/ibm-plex-mono-v4-latin-regular.ttf +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/ibm-plex-mono-v4-latin/ibm-plex-mono-v4-latin-regular.woff +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/ibm-plex-mono-v4-latin/ibm-plex-mono-v4-latin-regular.woff2 +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-700.eot +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-700.svg +438 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-700.ttf +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-700.woff +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-700.woff2 +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-regular.eot +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-regular.svg +435 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-regular.ttf +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-regular.woff +0 -0
- rasa/core/channels/inspector/src/theme/base/fonts/lato-v14-latin/lato-v14-latin-regular.woff2 +0 -0
- rasa/core/channels/inspector/src/theme/base/radii.ts +9 -0
- rasa/core/channels/inspector/src/theme/base/shadows.ts +7 -0
- rasa/core/channels/inspector/src/theme/base/sizes.ts +7 -0
- rasa/core/channels/inspector/src/theme/base/space.ts +15 -0
- rasa/core/channels/inspector/src/theme/base/styles.ts +13 -0
- rasa/core/channels/inspector/src/theme/base/typography.ts +24 -0
- rasa/core/channels/inspector/src/theme/base/zIndices.ts +19 -0
- rasa/core/channels/inspector/src/theme/index.ts +101 -0
- rasa/core/channels/inspector/src/types.ts +84 -0
- rasa/core/channels/inspector/src/vite-env.d.ts +1 -0
- rasa/core/channels/inspector/tests/__mocks__/fileMock.ts +1 -0
- rasa/core/channels/inspector/tests/__mocks__/matchMedia.ts +16 -0
- rasa/core/channels/inspector/tests/__mocks__/styleMock.ts +1 -0
- rasa/core/channels/inspector/tests/renderWithProviders.tsx +14 -0
- rasa/core/channels/inspector/tsconfig.json +26 -0
- rasa/core/channels/inspector/tsconfig.node.json +10 -0
- rasa/core/channels/inspector/vite.config.ts +8 -0
- rasa/core/channels/inspector/yarn.lock +6249 -0
- rasa/core/channels/mattermost.py +229 -0
- rasa/core/channels/rasa_chat.py +126 -0
- rasa/core/channels/rest.py +230 -0
- rasa/core/channels/rocketchat.py +174 -0
- rasa/core/channels/slack.py +620 -0
- rasa/core/channels/socketio.py +302 -0
- rasa/core/channels/telegram.py +298 -0
- rasa/core/channels/twilio.py +169 -0
- rasa/core/channels/vier_cvg.py +374 -0
- rasa/core/channels/voice_ready/__init__.py +0 -0
- rasa/core/channels/voice_ready/audiocodes.py +501 -0
- rasa/core/channels/voice_ready/jambonz.py +121 -0
- rasa/core/channels/voice_ready/jambonz_protocol.py +396 -0
- rasa/core/channels/voice_ready/twilio_voice.py +403 -0
- rasa/core/channels/voice_ready/utils.py +37 -0
- rasa/core/channels/voice_stream/__init__.py +0 -0
- rasa/core/channels/voice_stream/asr/__init__.py +0 -0
- rasa/core/channels/voice_stream/asr/asr_engine.py +89 -0
- rasa/core/channels/voice_stream/asr/asr_event.py +18 -0
- rasa/core/channels/voice_stream/asr/azure.py +130 -0
- rasa/core/channels/voice_stream/asr/deepgram.py +90 -0
- rasa/core/channels/voice_stream/audio_bytes.py +8 -0
- rasa/core/channels/voice_stream/browser_audio.py +107 -0
- rasa/core/channels/voice_stream/call_state.py +23 -0
- rasa/core/channels/voice_stream/tts/__init__.py +0 -0
- rasa/core/channels/voice_stream/tts/azure.py +106 -0
- rasa/core/channels/voice_stream/tts/cartesia.py +118 -0
- rasa/core/channels/voice_stream/tts/tts_cache.py +27 -0
- rasa/core/channels/voice_stream/tts/tts_engine.py +58 -0
- rasa/core/channels/voice_stream/twilio_media_streams.py +173 -0
- rasa/core/channels/voice_stream/util.py +57 -0
- rasa/core/channels/voice_stream/voice_channel.py +427 -0
- rasa/core/channels/webexteams.py +134 -0
- rasa/core/concurrent_lock_store.py +210 -0
- rasa/core/constants.py +112 -0
- rasa/core/evaluation/__init__.py +0 -0
- rasa/core/evaluation/marker.py +267 -0
- rasa/core/evaluation/marker_base.py +923 -0
- rasa/core/evaluation/marker_stats.py +293 -0
- rasa/core/evaluation/marker_tracker_loader.py +103 -0
- rasa/core/exceptions.py +29 -0
- rasa/core/exporter.py +284 -0
- rasa/core/featurizers/__init__.py +0 -0
- rasa/core/featurizers/precomputation.py +410 -0
- rasa/core/featurizers/single_state_featurizer.py +421 -0
- rasa/core/featurizers/tracker_featurizers.py +1262 -0
- rasa/core/http_interpreter.py +89 -0
- rasa/core/information_retrieval/__init__.py +7 -0
- rasa/core/information_retrieval/faiss.py +124 -0
- rasa/core/information_retrieval/information_retrieval.py +137 -0
- rasa/core/information_retrieval/milvus.py +59 -0
- rasa/core/information_retrieval/qdrant.py +96 -0
- rasa/core/jobs.py +63 -0
- rasa/core/lock.py +139 -0
- rasa/core/lock_store.py +343 -0
- rasa/core/migrate.py +403 -0
- rasa/core/nlg/__init__.py +3 -0
- rasa/core/nlg/callback.py +146 -0
- rasa/core/nlg/contextual_response_rephraser.py +320 -0
- rasa/core/nlg/generator.py +230 -0
- rasa/core/nlg/interpolator.py +143 -0
- rasa/core/nlg/response.py +155 -0
- rasa/core/nlg/summarize.py +70 -0
- rasa/core/persistor.py +538 -0
- rasa/core/policies/__init__.py +0 -0
- rasa/core/policies/ensemble.py +329 -0
- rasa/core/policies/enterprise_search_policy.py +905 -0
- rasa/core/policies/enterprise_search_prompt_template.jinja2 +25 -0
- rasa/core/policies/enterprise_search_prompt_with_citation_template.jinja2 +60 -0
- rasa/core/policies/flow_policy.py +205 -0
- rasa/core/policies/flows/__init__.py +0 -0
- rasa/core/policies/flows/flow_exceptions.py +44 -0
- rasa/core/policies/flows/flow_executor.py +754 -0
- rasa/core/policies/flows/flow_step_result.py +43 -0
- rasa/core/policies/intentless_policy.py +1031 -0
- rasa/core/policies/intentless_prompt_template.jinja2 +22 -0
- rasa/core/policies/memoization.py +538 -0
- rasa/core/policies/policy.py +725 -0
- rasa/core/policies/rule_policy.py +1273 -0
- rasa/core/policies/ted_policy.py +2169 -0
- rasa/core/policies/unexpected_intent_policy.py +1022 -0
- rasa/core/processor.py +1465 -0
- rasa/core/run.py +342 -0
- rasa/core/secrets_manager/__init__.py +0 -0
- rasa/core/secrets_manager/constants.py +36 -0
- rasa/core/secrets_manager/endpoints.py +391 -0
- rasa/core/secrets_manager/factory.py +241 -0
- rasa/core/secrets_manager/secret_manager.py +262 -0
- rasa/core/secrets_manager/vault.py +584 -0
- rasa/core/test.py +1335 -0
- rasa/core/tracker_store.py +1703 -0
- rasa/core/train.py +105 -0
- rasa/core/training/__init__.py +89 -0
- rasa/core/training/converters/__init__.py +0 -0
- rasa/core/training/converters/responses_prefix_converter.py +119 -0
- rasa/core/training/interactive.py +1744 -0
- rasa/core/training/story_conflict.py +381 -0
- rasa/core/training/training.py +93 -0
- rasa/core/utils.py +366 -0
- rasa/core/visualize.py +70 -0
- rasa/dialogue_understanding/__init__.py +0 -0
- rasa/dialogue_understanding/coexistence/__init__.py +0 -0
- rasa/dialogue_understanding/coexistence/constants.py +4 -0
- rasa/dialogue_understanding/coexistence/intent_based_router.py +196 -0
- rasa/dialogue_understanding/coexistence/llm_based_router.py +327 -0
- rasa/dialogue_understanding/coexistence/router_template.jinja2 +12 -0
- rasa/dialogue_understanding/commands/__init__.py +61 -0
- rasa/dialogue_understanding/commands/can_not_handle_command.py +70 -0
- rasa/dialogue_understanding/commands/cancel_flow_command.py +125 -0
- rasa/dialogue_understanding/commands/change_flow_command.py +44 -0
- rasa/dialogue_understanding/commands/chit_chat_answer_command.py +57 -0
- rasa/dialogue_understanding/commands/clarify_command.py +86 -0
- rasa/dialogue_understanding/commands/command.py +85 -0
- rasa/dialogue_understanding/commands/correct_slots_command.py +297 -0
- rasa/dialogue_understanding/commands/error_command.py +79 -0
- rasa/dialogue_understanding/commands/free_form_answer_command.py +9 -0
- rasa/dialogue_understanding/commands/handle_code_change_command.py +73 -0
- rasa/dialogue_understanding/commands/human_handoff_command.py +66 -0
- rasa/dialogue_understanding/commands/knowledge_answer_command.py +57 -0
- rasa/dialogue_understanding/commands/noop_command.py +54 -0
- rasa/dialogue_understanding/commands/repeat_bot_messages_command.py +60 -0
- rasa/dialogue_understanding/commands/restart_command.py +58 -0
- rasa/dialogue_understanding/commands/session_end_command.py +61 -0
- rasa/dialogue_understanding/commands/session_start_command.py +59 -0
- rasa/dialogue_understanding/commands/set_slot_command.py +160 -0
- rasa/dialogue_understanding/commands/skip_question_command.py +75 -0
- rasa/dialogue_understanding/commands/start_flow_command.py +107 -0
- rasa/dialogue_understanding/commands/user_silence_command.py +59 -0
- rasa/dialogue_understanding/commands/utils.py +45 -0
- rasa/dialogue_understanding/generator/__init__.py +21 -0
- rasa/dialogue_understanding/generator/command_generator.py +464 -0
- rasa/dialogue_understanding/generator/constants.py +27 -0
- rasa/dialogue_understanding/generator/flow_document_template.jinja2 +4 -0
- rasa/dialogue_understanding/generator/flow_retrieval.py +466 -0
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +500 -0
- rasa/dialogue_understanding/generator/llm_command_generator.py +67 -0
- rasa/dialogue_understanding/generator/multi_step/__init__.py +0 -0
- rasa/dialogue_understanding/generator/multi_step/fill_slots_prompt.jinja2 +62 -0
- rasa/dialogue_understanding/generator/multi_step/handle_flows_prompt.jinja2 +38 -0
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +920 -0
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +261 -0
- rasa/dialogue_understanding/generator/single_step/__init__.py +0 -0
- rasa/dialogue_understanding/generator/single_step/command_prompt_template.jinja2 +60 -0
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +486 -0
- rasa/dialogue_understanding/patterns/__init__.py +0 -0
- rasa/dialogue_understanding/patterns/cancel.py +111 -0
- rasa/dialogue_understanding/patterns/cannot_handle.py +43 -0
- rasa/dialogue_understanding/patterns/chitchat.py +37 -0
- rasa/dialogue_understanding/patterns/clarify.py +97 -0
- rasa/dialogue_understanding/patterns/code_change.py +41 -0
- rasa/dialogue_understanding/patterns/collect_information.py +90 -0
- rasa/dialogue_understanding/patterns/completed.py +40 -0
- rasa/dialogue_understanding/patterns/continue_interrupted.py +42 -0
- rasa/dialogue_understanding/patterns/correction.py +278 -0
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +301 -0
- rasa/dialogue_understanding/patterns/human_handoff.py +37 -0
- rasa/dialogue_understanding/patterns/internal_error.py +47 -0
- rasa/dialogue_understanding/patterns/repeat.py +37 -0
- rasa/dialogue_understanding/patterns/restart.py +37 -0
- rasa/dialogue_understanding/patterns/search.py +37 -0
- rasa/dialogue_understanding/patterns/session_start.py +37 -0
- rasa/dialogue_understanding/patterns/skip_question.py +38 -0
- rasa/dialogue_understanding/patterns/user_silence.py +37 -0
- rasa/dialogue_understanding/processor/__init__.py +0 -0
- rasa/dialogue_understanding/processor/command_processor.py +720 -0
- rasa/dialogue_understanding/processor/command_processor_component.py +43 -0
- rasa/dialogue_understanding/stack/__init__.py +0 -0
- rasa/dialogue_understanding/stack/dialogue_stack.py +178 -0
- rasa/dialogue_understanding/stack/frames/__init__.py +19 -0
- rasa/dialogue_understanding/stack/frames/chit_chat_frame.py +27 -0
- rasa/dialogue_understanding/stack/frames/dialogue_stack_frame.py +137 -0
- rasa/dialogue_understanding/stack/frames/flow_stack_frame.py +157 -0
- rasa/dialogue_understanding/stack/frames/pattern_frame.py +10 -0
- rasa/dialogue_understanding/stack/frames/search_frame.py +27 -0
- rasa/dialogue_understanding/stack/utils.py +211 -0
- rasa/dialogue_understanding/utils.py +14 -0
- rasa/dialogue_understanding_test/__init__.py +0 -0
- rasa/dialogue_understanding_test/command_metric_calculation.py +12 -0
- rasa/dialogue_understanding_test/constants.py +17 -0
- rasa/dialogue_understanding_test/du_test_case.py +118 -0
- rasa/dialogue_understanding_test/du_test_result.py +11 -0
- rasa/dialogue_understanding_test/du_test_runner.py +93 -0
- rasa/dialogue_understanding_test/io.py +54 -0
- rasa/dialogue_understanding_test/validation.py +22 -0
- rasa/e2e_test/__init__.py +0 -0
- rasa/e2e_test/aggregate_test_stats_calculator.py +134 -0
- rasa/e2e_test/assertions.py +1345 -0
- rasa/e2e_test/assertions_schema.yml +129 -0
- rasa/e2e_test/constants.py +31 -0
- rasa/e2e_test/e2e_config.py +220 -0
- rasa/e2e_test/e2e_config_schema.yml +26 -0
- rasa/e2e_test/e2e_test_case.py +569 -0
- rasa/e2e_test/e2e_test_converter.py +363 -0
- rasa/e2e_test/e2e_test_converter_prompt.jinja2 +70 -0
- rasa/e2e_test/e2e_test_coverage_report.py +364 -0
- rasa/e2e_test/e2e_test_result.py +54 -0
- rasa/e2e_test/e2e_test_runner.py +1192 -0
- rasa/e2e_test/e2e_test_schema.yml +181 -0
- rasa/e2e_test/pykwalify_extensions.py +39 -0
- rasa/e2e_test/stub_custom_action.py +70 -0
- rasa/e2e_test/utils/__init__.py +0 -0
- rasa/e2e_test/utils/e2e_yaml_utils.py +55 -0
- rasa/e2e_test/utils/io.py +598 -0
- rasa/e2e_test/utils/validation.py +178 -0
- rasa/engine/__init__.py +0 -0
- rasa/engine/caching.py +463 -0
- rasa/engine/constants.py +17 -0
- rasa/engine/exceptions.py +14 -0
- rasa/engine/graph.py +642 -0
- rasa/engine/loader.py +48 -0
- rasa/engine/recipes/__init__.py +0 -0
- rasa/engine/recipes/config_files/default_config.yml +41 -0
- rasa/engine/recipes/default_components.py +97 -0
- rasa/engine/recipes/default_recipe.py +1272 -0
- rasa/engine/recipes/graph_recipe.py +79 -0
- rasa/engine/recipes/recipe.py +93 -0
- rasa/engine/runner/__init__.py +0 -0
- rasa/engine/runner/dask.py +250 -0
- rasa/engine/runner/interface.py +49 -0
- rasa/engine/storage/__init__.py +0 -0
- rasa/engine/storage/local_model_storage.py +244 -0
- rasa/engine/storage/resource.py +110 -0
- rasa/engine/storage/storage.py +199 -0
- rasa/engine/training/__init__.py +0 -0
- rasa/engine/training/components.py +176 -0
- rasa/engine/training/fingerprinting.py +64 -0
- rasa/engine/training/graph_trainer.py +256 -0
- rasa/engine/training/hooks.py +164 -0
- rasa/engine/validation.py +1451 -0
- rasa/env.py +14 -0
- rasa/exceptions.py +69 -0
- rasa/graph_components/__init__.py +0 -0
- rasa/graph_components/converters/__init__.py +0 -0
- rasa/graph_components/converters/nlu_message_converter.py +48 -0
- rasa/graph_components/providers/__init__.py +0 -0
- rasa/graph_components/providers/domain_for_core_training_provider.py +87 -0
- rasa/graph_components/providers/domain_provider.py +71 -0
- rasa/graph_components/providers/flows_provider.py +74 -0
- rasa/graph_components/providers/forms_provider.py +44 -0
- rasa/graph_components/providers/nlu_training_data_provider.py +56 -0
- rasa/graph_components/providers/responses_provider.py +44 -0
- rasa/graph_components/providers/rule_only_provider.py +49 -0
- rasa/graph_components/providers/story_graph_provider.py +96 -0
- rasa/graph_components/providers/training_tracker_provider.py +55 -0
- rasa/graph_components/validators/__init__.py +0 -0
- rasa/graph_components/validators/default_recipe_validator.py +550 -0
- rasa/graph_components/validators/finetuning_validator.py +302 -0
- rasa/hooks.py +111 -0
- rasa/jupyter.py +63 -0
- rasa/llm_fine_tuning/__init__.py +0 -0
- rasa/llm_fine_tuning/annotation_module.py +241 -0
- rasa/llm_fine_tuning/conversations.py +144 -0
- rasa/llm_fine_tuning/llm_data_preparation_module.py +178 -0
- rasa/llm_fine_tuning/paraphrasing/__init__.py +0 -0
- rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +281 -0
- rasa/llm_fine_tuning/paraphrasing/default_rephrase_prompt_template.jina2 +44 -0
- rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +121 -0
- rasa/llm_fine_tuning/paraphrasing/rephrased_user_message.py +10 -0
- rasa/llm_fine_tuning/paraphrasing_module.py +128 -0
- rasa/llm_fine_tuning/storage.py +174 -0
- rasa/llm_fine_tuning/train_test_split_module.py +441 -0
- rasa/markers/__init__.py +0 -0
- rasa/markers/marker.py +269 -0
- rasa/markers/marker_base.py +828 -0
- rasa/markers/upload.py +74 -0
- rasa/markers/validate.py +21 -0
- rasa/model.py +118 -0
- rasa/model_manager/__init__.py +0 -0
- rasa/model_manager/config.py +40 -0
- rasa/model_manager/model_api.py +559 -0
- rasa/model_manager/runner_service.py +286 -0
- rasa/model_manager/socket_bridge.py +146 -0
- rasa/model_manager/studio_jwt_auth.py +86 -0
- rasa/model_manager/trainer_service.py +325 -0
- rasa/model_manager/utils.py +87 -0
- rasa/model_manager/warm_rasa_process.py +187 -0
- rasa/model_service.py +112 -0
- rasa/model_testing.py +457 -0
- rasa/model_training.py +596 -0
- rasa/nlu/__init__.py +7 -0
- rasa/nlu/classifiers/__init__.py +3 -0
- rasa/nlu/classifiers/classifier.py +5 -0
- rasa/nlu/classifiers/diet_classifier.py +1881 -0
- rasa/nlu/classifiers/fallback_classifier.py +192 -0
- rasa/nlu/classifiers/keyword_intent_classifier.py +188 -0
- rasa/nlu/classifiers/logistic_regression_classifier.py +253 -0
- rasa/nlu/classifiers/mitie_intent_classifier.py +156 -0
- rasa/nlu/classifiers/regex_message_handler.py +56 -0
- rasa/nlu/classifiers/sklearn_intent_classifier.py +330 -0
- rasa/nlu/constants.py +77 -0
- rasa/nlu/convert.py +40 -0
- rasa/nlu/emulators/__init__.py +0 -0
- rasa/nlu/emulators/dialogflow.py +55 -0
- rasa/nlu/emulators/emulator.py +49 -0
- rasa/nlu/emulators/luis.py +86 -0
- rasa/nlu/emulators/no_emulator.py +10 -0
- rasa/nlu/emulators/wit.py +56 -0
- rasa/nlu/extractors/__init__.py +0 -0
- rasa/nlu/extractors/crf_entity_extractor.py +715 -0
- rasa/nlu/extractors/duckling_entity_extractor.py +206 -0
- rasa/nlu/extractors/entity_synonyms.py +178 -0
- rasa/nlu/extractors/extractor.py +470 -0
- rasa/nlu/extractors/mitie_entity_extractor.py +293 -0
- rasa/nlu/extractors/regex_entity_extractor.py +220 -0
- rasa/nlu/extractors/spacy_entity_extractor.py +95 -0
- rasa/nlu/featurizers/__init__.py +0 -0
- rasa/nlu/featurizers/dense_featurizer/__init__.py +0 -0
- rasa/nlu/featurizers/dense_featurizer/convert_featurizer.py +445 -0
- rasa/nlu/featurizers/dense_featurizer/dense_featurizer.py +57 -0
- rasa/nlu/featurizers/dense_featurizer/lm_featurizer.py +768 -0
- rasa/nlu/featurizers/dense_featurizer/mitie_featurizer.py +170 -0
- rasa/nlu/featurizers/dense_featurizer/spacy_featurizer.py +132 -0
- rasa/nlu/featurizers/featurizer.py +89 -0
- rasa/nlu/featurizers/sparse_featurizer/__init__.py +0 -0
- rasa/nlu/featurizers/sparse_featurizer/count_vectors_featurizer.py +867 -0
- rasa/nlu/featurizers/sparse_featurizer/lexical_syntactic_featurizer.py +571 -0
- rasa/nlu/featurizers/sparse_featurizer/regex_featurizer.py +271 -0
- rasa/nlu/featurizers/sparse_featurizer/sparse_featurizer.py +9 -0
- rasa/nlu/model.py +24 -0
- rasa/nlu/run.py +27 -0
- rasa/nlu/selectors/__init__.py +0 -0
- rasa/nlu/selectors/response_selector.py +987 -0
- rasa/nlu/test.py +1940 -0
- rasa/nlu/tokenizers/__init__.py +0 -0
- rasa/nlu/tokenizers/jieba_tokenizer.py +148 -0
- rasa/nlu/tokenizers/mitie_tokenizer.py +75 -0
- rasa/nlu/tokenizers/spacy_tokenizer.py +72 -0
- rasa/nlu/tokenizers/tokenizer.py +239 -0
- rasa/nlu/tokenizers/whitespace_tokenizer.py +95 -0
- rasa/nlu/utils/__init__.py +35 -0
- rasa/nlu/utils/bilou_utils.py +462 -0
- rasa/nlu/utils/hugging_face/__init__.py +0 -0
- rasa/nlu/utils/hugging_face/registry.py +108 -0
- rasa/nlu/utils/hugging_face/transformers_pre_post_processors.py +311 -0
- rasa/nlu/utils/mitie_utils.py +113 -0
- rasa/nlu/utils/pattern_utils.py +168 -0
- rasa/nlu/utils/spacy_utils.py +310 -0
- rasa/plugin.py +90 -0
- rasa/server.py +1588 -0
- rasa/shared/__init__.py +0 -0
- rasa/shared/constants.py +311 -0
- rasa/shared/core/__init__.py +0 -0
- rasa/shared/core/command_payload_reader.py +109 -0
- rasa/shared/core/constants.py +180 -0
- rasa/shared/core/conversation.py +46 -0
- rasa/shared/core/domain.py +2172 -0
- rasa/shared/core/events.py +2559 -0
- rasa/shared/core/flows/__init__.py +7 -0
- rasa/shared/core/flows/flow.py +562 -0
- rasa/shared/core/flows/flow_path.py +84 -0
- rasa/shared/core/flows/flow_step.py +146 -0
- rasa/shared/core/flows/flow_step_links.py +319 -0
- rasa/shared/core/flows/flow_step_sequence.py +70 -0
- rasa/shared/core/flows/flows_list.py +258 -0
- rasa/shared/core/flows/flows_yaml_schema.json +303 -0
- rasa/shared/core/flows/nlu_trigger.py +117 -0
- rasa/shared/core/flows/steps/__init__.py +24 -0
- rasa/shared/core/flows/steps/action.py +56 -0
- rasa/shared/core/flows/steps/call.py +64 -0
- rasa/shared/core/flows/steps/collect.py +112 -0
- rasa/shared/core/flows/steps/constants.py +5 -0
- rasa/shared/core/flows/steps/continuation.py +36 -0
- rasa/shared/core/flows/steps/end.py +22 -0
- rasa/shared/core/flows/steps/internal.py +44 -0
- rasa/shared/core/flows/steps/link.py +51 -0
- rasa/shared/core/flows/steps/no_operation.py +48 -0
- rasa/shared/core/flows/steps/set_slots.py +50 -0
- rasa/shared/core/flows/steps/start.py +30 -0
- rasa/shared/core/flows/utils.py +39 -0
- rasa/shared/core/flows/validation.py +735 -0
- rasa/shared/core/flows/yaml_flows_io.py +405 -0
- rasa/shared/core/generator.py +908 -0
- rasa/shared/core/slot_mappings.py +526 -0
- rasa/shared/core/slots.py +654 -0
- rasa/shared/core/trackers.py +1183 -0
- rasa/shared/core/training_data/__init__.py +0 -0
- rasa/shared/core/training_data/loading.py +89 -0
- rasa/shared/core/training_data/story_reader/__init__.py +0 -0
- rasa/shared/core/training_data/story_reader/story_reader.py +129 -0
- rasa/shared/core/training_data/story_reader/story_step_builder.py +168 -0
- rasa/shared/core/training_data/story_reader/yaml_story_reader.py +888 -0
- rasa/shared/core/training_data/story_writer/__init__.py +0 -0
- rasa/shared/core/training_data/story_writer/story_writer.py +76 -0
- rasa/shared/core/training_data/story_writer/yaml_story_writer.py +444 -0
- rasa/shared/core/training_data/structures.py +858 -0
- rasa/shared/core/training_data/visualization.html +146 -0
- rasa/shared/core/training_data/visualization.py +603 -0
- rasa/shared/data.py +249 -0
- rasa/shared/engine/__init__.py +0 -0
- rasa/shared/engine/caching.py +26 -0
- rasa/shared/exceptions.py +167 -0
- rasa/shared/importers/__init__.py +0 -0
- rasa/shared/importers/importer.py +770 -0
- rasa/shared/importers/multi_project.py +215 -0
- rasa/shared/importers/rasa.py +108 -0
- rasa/shared/importers/remote_importer.py +196 -0
- rasa/shared/importers/utils.py +36 -0
- rasa/shared/nlu/__init__.py +0 -0
- rasa/shared/nlu/constants.py +53 -0
- rasa/shared/nlu/interpreter.py +10 -0
- rasa/shared/nlu/training_data/__init__.py +0 -0
- rasa/shared/nlu/training_data/entities_parser.py +208 -0
- rasa/shared/nlu/training_data/features.py +492 -0
- rasa/shared/nlu/training_data/formats/__init__.py +10 -0
- rasa/shared/nlu/training_data/formats/dialogflow.py +163 -0
- rasa/shared/nlu/training_data/formats/luis.py +87 -0
- rasa/shared/nlu/training_data/formats/rasa.py +135 -0
- rasa/shared/nlu/training_data/formats/rasa_yaml.py +618 -0
- rasa/shared/nlu/training_data/formats/readerwriter.py +244 -0
- rasa/shared/nlu/training_data/formats/wit.py +52 -0
- rasa/shared/nlu/training_data/loading.py +137 -0
- rasa/shared/nlu/training_data/lookup_tables_parser.py +30 -0
- rasa/shared/nlu/training_data/message.py +490 -0
- rasa/shared/nlu/training_data/schemas/__init__.py +0 -0
- rasa/shared/nlu/training_data/schemas/data_schema.py +85 -0
- rasa/shared/nlu/training_data/schemas/nlu.yml +53 -0
- rasa/shared/nlu/training_data/schemas/responses.yml +70 -0
- rasa/shared/nlu/training_data/synonyms_parser.py +42 -0
- rasa/shared/nlu/training_data/training_data.py +729 -0
- rasa/shared/nlu/training_data/util.py +223 -0
- rasa/shared/providers/__init__.py +0 -0
- rasa/shared/providers/_configs/__init__.py +0 -0
- rasa/shared/providers/_configs/azure_openai_client_config.py +677 -0
- rasa/shared/providers/_configs/client_config.py +59 -0
- rasa/shared/providers/_configs/default_litellm_client_config.py +132 -0
- rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +236 -0
- rasa/shared/providers/_configs/litellm_router_client_config.py +222 -0
- rasa/shared/providers/_configs/model_group_config.py +173 -0
- rasa/shared/providers/_configs/openai_client_config.py +177 -0
- rasa/shared/providers/_configs/rasa_llm_client_config.py +75 -0
- rasa/shared/providers/_configs/self_hosted_llm_client_config.py +178 -0
- rasa/shared/providers/_configs/utils.py +117 -0
- rasa/shared/providers/_ssl_verification_utils.py +124 -0
- rasa/shared/providers/_utils.py +79 -0
- rasa/shared/providers/constants.py +7 -0
- rasa/shared/providers/embedding/__init__.py +0 -0
- rasa/shared/providers/embedding/_base_litellm_embedding_client.py +243 -0
- rasa/shared/providers/embedding/_langchain_embedding_client_adapter.py +74 -0
- rasa/shared/providers/embedding/azure_openai_embedding_client.py +335 -0
- rasa/shared/providers/embedding/default_litellm_embedding_client.py +126 -0
- rasa/shared/providers/embedding/embedding_client.py +90 -0
- rasa/shared/providers/embedding/embedding_response.py +41 -0
- rasa/shared/providers/embedding/huggingface_local_embedding_client.py +191 -0
- rasa/shared/providers/embedding/litellm_router_embedding_client.py +138 -0
- rasa/shared/providers/embedding/openai_embedding_client.py +172 -0
- rasa/shared/providers/llm/__init__.py +0 -0
- rasa/shared/providers/llm/_base_litellm_client.py +265 -0
- rasa/shared/providers/llm/azure_openai_llm_client.py +415 -0
- rasa/shared/providers/llm/default_litellm_llm_client.py +110 -0
- rasa/shared/providers/llm/litellm_router_llm_client.py +202 -0
- rasa/shared/providers/llm/llm_client.py +78 -0
- rasa/shared/providers/llm/llm_response.py +50 -0
- rasa/shared/providers/llm/openai_llm_client.py +161 -0
- rasa/shared/providers/llm/rasa_llm_client.py +120 -0
- rasa/shared/providers/llm/self_hosted_llm_client.py +276 -0
- rasa/shared/providers/mappings.py +94 -0
- rasa/shared/providers/router/__init__.py +0 -0
- rasa/shared/providers/router/_base_litellm_router_client.py +185 -0
- rasa/shared/providers/router/router_client.py +75 -0
- rasa/shared/utils/__init__.py +0 -0
- rasa/shared/utils/cli.py +102 -0
- rasa/shared/utils/common.py +324 -0
- rasa/shared/utils/constants.py +4 -0
- rasa/shared/utils/health_check/__init__.py +0 -0
- rasa/shared/utils/health_check/embeddings_health_check_mixin.py +31 -0
- rasa/shared/utils/health_check/health_check.py +258 -0
- rasa/shared/utils/health_check/llm_health_check_mixin.py +31 -0
- rasa/shared/utils/io.py +499 -0
- rasa/shared/utils/llm.py +764 -0
- rasa/shared/utils/pykwalify_extensions.py +27 -0
- rasa/shared/utils/schemas/__init__.py +0 -0
- rasa/shared/utils/schemas/config.yml +2 -0
- rasa/shared/utils/schemas/domain.yml +145 -0
- rasa/shared/utils/schemas/events.py +214 -0
- rasa/shared/utils/schemas/model_config.yml +36 -0
- rasa/shared/utils/schemas/stories.yml +173 -0
- rasa/shared/utils/yaml.py +1068 -0
- rasa/studio/__init__.py +0 -0
- rasa/studio/auth.py +270 -0
- rasa/studio/config.py +136 -0
- rasa/studio/constants.py +19 -0
- rasa/studio/data_handler.py +368 -0
- rasa/studio/download.py +489 -0
- rasa/studio/results_logger.py +137 -0
- rasa/studio/train.py +134 -0
- rasa/studio/upload.py +563 -0
- rasa/telemetry.py +1876 -0
- rasa/tracing/__init__.py +0 -0
- rasa/tracing/config.py +355 -0
- rasa/tracing/constants.py +62 -0
- rasa/tracing/instrumentation/__init__.py +0 -0
- rasa/tracing/instrumentation/attribute_extractors.py +765 -0
- rasa/tracing/instrumentation/instrumentation.py +1306 -0
- rasa/tracing/instrumentation/intentless_policy_instrumentation.py +144 -0
- rasa/tracing/instrumentation/metrics.py +294 -0
- rasa/tracing/metric_instrument_provider.py +205 -0
- rasa/utils/__init__.py +0 -0
- rasa/utils/beta.py +83 -0
- rasa/utils/cli.py +28 -0
- rasa/utils/common.py +639 -0
- rasa/utils/converter.py +53 -0
- rasa/utils/endpoints.py +331 -0
- rasa/utils/io.py +252 -0
- rasa/utils/json_utils.py +60 -0
- rasa/utils/licensing.py +542 -0
- rasa/utils/log_utils.py +181 -0
- rasa/utils/mapper.py +210 -0
- rasa/utils/ml_utils.py +147 -0
- rasa/utils/plotting.py +362 -0
- rasa/utils/sanic_error_handler.py +32 -0
- rasa/utils/singleton.py +23 -0
- rasa/utils/tensorflow/__init__.py +0 -0
- rasa/utils/tensorflow/callback.py +112 -0
- rasa/utils/tensorflow/constants.py +116 -0
- rasa/utils/tensorflow/crf.py +492 -0
- rasa/utils/tensorflow/data_generator.py +440 -0
- rasa/utils/tensorflow/environment.py +161 -0
- rasa/utils/tensorflow/exceptions.py +5 -0
- rasa/utils/tensorflow/feature_array.py +366 -0
- rasa/utils/tensorflow/layers.py +1565 -0
- rasa/utils/tensorflow/layers_utils.py +113 -0
- rasa/utils/tensorflow/metrics.py +281 -0
- rasa/utils/tensorflow/model_data.py +798 -0
- rasa/utils/tensorflow/model_data_utils.py +499 -0
- rasa/utils/tensorflow/models.py +935 -0
- rasa/utils/tensorflow/rasa_layers.py +1094 -0
- rasa/utils/tensorflow/transformer.py +640 -0
- rasa/utils/tensorflow/types.py +6 -0
- rasa/utils/train_utils.py +572 -0
- rasa/utils/url_tools.py +53 -0
- rasa/utils/yaml.py +54 -0
- rasa/validator.py +1644 -0
- rasa/version.py +3 -0
- rasa_pro-3.12.0.dev1.dist-info/METADATA +199 -0
- rasa_pro-3.12.0.dev1.dist-info/NOTICE +5 -0
- rasa_pro-3.12.0.dev1.dist-info/RECORD +790 -0
- rasa_pro-3.12.0.dev1.dist-info/WHEEL +4 -0
- rasa_pro-3.12.0.dev1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,1744 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import textwrap
|
|
5
|
+
import uuid
|
|
6
|
+
import warnings
|
|
7
|
+
from functools import partial
|
|
8
|
+
from multiprocessing import Process
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Callable,
|
|
12
|
+
Deque,
|
|
13
|
+
Dict,
|
|
14
|
+
List,
|
|
15
|
+
Optional,
|
|
16
|
+
Text,
|
|
17
|
+
Tuple,
|
|
18
|
+
Union,
|
|
19
|
+
Set,
|
|
20
|
+
cast,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import questionary
|
|
25
|
+
import terminaltables.width_and_alignment
|
|
26
|
+
from aiohttp import ClientError
|
|
27
|
+
from colorclass import Color
|
|
28
|
+
from questionary import Choice, Form, Question
|
|
29
|
+
from sanic import Sanic, response
|
|
30
|
+
from sanic.exceptions import NotFound
|
|
31
|
+
from sanic.request import Request
|
|
32
|
+
from sanic.response import HTTPResponse
|
|
33
|
+
from terminaltables import AsciiTable, SingleTable
|
|
34
|
+
|
|
35
|
+
import rasa.cli.utils
|
|
36
|
+
import rasa.core.train
|
|
37
|
+
import rasa.shared.core.events
|
|
38
|
+
import rasa.shared.data
|
|
39
|
+
import rasa.shared.utils.cli
|
|
40
|
+
import rasa.shared.utils.io
|
|
41
|
+
|
|
42
|
+
# WARNING: This command line UI is using an external library
|
|
43
|
+
# communicating with the shell - these functions are hard to test
|
|
44
|
+
# automatically. If you change anything in here, please make sure to
|
|
45
|
+
# run the interactive learning and check if your part of the "ui"
|
|
46
|
+
# still works.
|
|
47
|
+
import rasa.utils.io as io_utils
|
|
48
|
+
from rasa import telemetry
|
|
49
|
+
from rasa.core import run, utils
|
|
50
|
+
from rasa.core.constants import DEFAULT_SERVER_FORMAT, DEFAULT_SERVER_PORT
|
|
51
|
+
from rasa.core.utils import AvailableEndpoints
|
|
52
|
+
from rasa.shared.constants import (
|
|
53
|
+
INTENT_MESSAGE_PREFIX,
|
|
54
|
+
DEFAULT_SENDER_ID,
|
|
55
|
+
UTTER_PREFIX,
|
|
56
|
+
DOCS_URL_NLU_BASED_POLICIES,
|
|
57
|
+
)
|
|
58
|
+
from rasa.shared.core.constants import (
|
|
59
|
+
USER_INTENT_RESTART,
|
|
60
|
+
ACTION_LISTEN_NAME,
|
|
61
|
+
LOOP_NAME,
|
|
62
|
+
ACTIVE_LOOP,
|
|
63
|
+
LOOP_REJECTED,
|
|
64
|
+
REQUESTED_SLOT,
|
|
65
|
+
LOOP_INTERRUPTED,
|
|
66
|
+
ACTION_UNLIKELY_INTENT_NAME,
|
|
67
|
+
)
|
|
68
|
+
from rasa.shared.core.domain import (
|
|
69
|
+
Domain,
|
|
70
|
+
KEY_INTENTS,
|
|
71
|
+
KEY_ENTITIES,
|
|
72
|
+
KEY_RESPONSES,
|
|
73
|
+
KEY_ACTIONS,
|
|
74
|
+
KEY_RESPONSES_TEXT,
|
|
75
|
+
)
|
|
76
|
+
from rasa.shared.core.events import (
|
|
77
|
+
ActionExecuted,
|
|
78
|
+
ActionReverted,
|
|
79
|
+
BotUttered,
|
|
80
|
+
Event,
|
|
81
|
+
Restarted,
|
|
82
|
+
UserUttered,
|
|
83
|
+
UserUtteranceReverted,
|
|
84
|
+
)
|
|
85
|
+
from rasa.shared.core.generator import TrackerWithCachedStates
|
|
86
|
+
from rasa.shared.core.trackers import EventVerbosity, DialogueStateTracker
|
|
87
|
+
from rasa.shared.core.training_data import visualization
|
|
88
|
+
from rasa.shared.core.training_data.visualization import (
|
|
89
|
+
VISUALIZATION_TEMPLATE_PATH,
|
|
90
|
+
visualize_neighborhood,
|
|
91
|
+
)
|
|
92
|
+
from rasa.shared.exceptions import InvalidConfigException
|
|
93
|
+
from rasa.shared.importers.rasa import TrainingDataImporter
|
|
94
|
+
from rasa.shared.nlu.constants import TEXT, INTENT_NAME_KEY
|
|
95
|
+
|
|
96
|
+
# noinspection PyProtectedMember
|
|
97
|
+
from rasa.shared.nlu.training_data import loading
|
|
98
|
+
from rasa.shared.nlu.training_data.loading import RASA, RASA_YAML
|
|
99
|
+
from rasa.shared.nlu.training_data.message import Message
|
|
100
|
+
from rasa.utils.common import update_sanic_log_level
|
|
101
|
+
from rasa.utils.endpoints import EndpointConfig
|
|
102
|
+
|
|
103
|
+
logger = logging.getLogger(__name__)
|
|
104
|
+
|
|
105
|
+
PATHS = {
|
|
106
|
+
"stories": "data/stories.yml",
|
|
107
|
+
"nlu": "data/nlu.yml",
|
|
108
|
+
"backup": "data/nlu_interactive.yml",
|
|
109
|
+
"domain": "domain.yml",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
SAVE_IN_E2E = False
|
|
113
|
+
|
|
114
|
+
# choose other intent, making sure this doesn't clash with an existing intent
|
|
115
|
+
OTHER_INTENT = uuid.uuid4().hex
|
|
116
|
+
OTHER_ACTION = uuid.uuid4().hex
|
|
117
|
+
NEW_ACTION = uuid.uuid4().hex
|
|
118
|
+
|
|
119
|
+
NEW_RESPONSES: Dict[Text, List[Dict[Text, Any]]] = {}
|
|
120
|
+
|
|
121
|
+
MAX_NUMBER_OF_TRAINING_STORIES_FOR_VISUALIZATION = 200
|
|
122
|
+
|
|
123
|
+
DEFAULT_STORY_GRAPH_FILE = "story_graph.dot"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class RestartConversation(Exception):
|
|
127
|
+
"""Exception used to break out the flow and restart the conversation."""
|
|
128
|
+
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ForkTracker(Exception):
|
|
133
|
+
"""Exception used to break out the flow and fork at a previous step.
|
|
134
|
+
|
|
135
|
+
The tracker will be reset to the selected point in the past and the
|
|
136
|
+
conversation will continue from there.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class UndoLastStep(Exception):
|
|
143
|
+
"""Exception used to break out the flow and undo the last step.
|
|
144
|
+
|
|
145
|
+
The last step is either the most recent user message or the most
|
|
146
|
+
recent action run by the bot.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Abort(Exception):
|
|
153
|
+
"""Exception used to abort the interactive learning and exit."""
|
|
154
|
+
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def send_message(
|
|
159
|
+
endpoint: EndpointConfig,
|
|
160
|
+
conversation_id: Text,
|
|
161
|
+
message: Text,
|
|
162
|
+
parse_data: Optional[Dict[Text, Any]] = None,
|
|
163
|
+
) -> Optional[Any]:
|
|
164
|
+
"""Send a user message to a conversation."""
|
|
165
|
+
payload = {
|
|
166
|
+
"sender": UserUttered.type_name,
|
|
167
|
+
"text": message,
|
|
168
|
+
"parse_data": parse_data,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return await endpoint.request(
|
|
172
|
+
json=payload,
|
|
173
|
+
method="post",
|
|
174
|
+
subpath=f"/conversations/{conversation_id}/messages",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def request_prediction(
|
|
179
|
+
endpoint: EndpointConfig, conversation_id: Text
|
|
180
|
+
) -> Optional[Any]:
|
|
181
|
+
"""Request the next action prediction from core."""
|
|
182
|
+
return await endpoint.request(
|
|
183
|
+
method="post", subpath=f"/conversations/{conversation_id}/predict"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def retrieve_domain(endpoint: EndpointConfig) -> Optional[Any]:
|
|
188
|
+
"""Retrieve the domain from core."""
|
|
189
|
+
return await endpoint.request(
|
|
190
|
+
method="get", subpath="/domain", headers={"Accept": "application/json"}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def retrieve_status(endpoint: EndpointConfig) -> Optional[Any]:
|
|
195
|
+
"""Retrieve the status from core."""
|
|
196
|
+
return await endpoint.request(method="get", subpath="/status")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def retrieve_tracker(
|
|
200
|
+
endpoint: EndpointConfig,
|
|
201
|
+
conversation_id: Text,
|
|
202
|
+
verbosity: EventVerbosity = EventVerbosity.ALL,
|
|
203
|
+
) -> Dict[Text, Any]:
|
|
204
|
+
"""Retrieve a tracker from core."""
|
|
205
|
+
path = f"/conversations/{conversation_id}/tracker?include_events={verbosity.name}"
|
|
206
|
+
result = await endpoint.request(
|
|
207
|
+
method="get", subpath=path, headers={"Accept": "application/json"}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# If the request wasn't successful the previous call had already raised. Hence,
|
|
211
|
+
# we can be sure we have the tracker in the right format.
|
|
212
|
+
return cast(Dict[Text, Any], result)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def send_action(
|
|
216
|
+
endpoint: EndpointConfig,
|
|
217
|
+
conversation_id: Text,
|
|
218
|
+
action_name: Text,
|
|
219
|
+
policy: Optional[Text] = None,
|
|
220
|
+
confidence: Optional[float] = None,
|
|
221
|
+
is_new_action: bool = False,
|
|
222
|
+
) -> Optional[Any]:
|
|
223
|
+
"""Log an action to a conversation."""
|
|
224
|
+
payload = ActionExecuted(action_name, policy, confidence).as_dict()
|
|
225
|
+
|
|
226
|
+
subpath = f"/conversations/{conversation_id}/execute"
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
return await endpoint.request(json=payload, method="post", subpath=subpath)
|
|
230
|
+
except ClientError:
|
|
231
|
+
if is_new_action:
|
|
232
|
+
if action_name in NEW_RESPONSES:
|
|
233
|
+
warning_questions = questionary.confirm(
|
|
234
|
+
f"WARNING: You have created a new action: '{action_name}', "
|
|
235
|
+
f"with matching response: "
|
|
236
|
+
f"'{NEW_RESPONSES[action_name][0][KEY_RESPONSES_TEXT]}'. "
|
|
237
|
+
f"This action will not return its message in this session, "
|
|
238
|
+
f"but the new response will be saved to your domain file "
|
|
239
|
+
f"when you exit and save this session. "
|
|
240
|
+
f"You do not need to do anything further."
|
|
241
|
+
)
|
|
242
|
+
await _ask_questions(warning_questions, conversation_id, endpoint)
|
|
243
|
+
else:
|
|
244
|
+
warning_questions = questionary.confirm(
|
|
245
|
+
f"WARNING: You have created a new action: '{action_name}', "
|
|
246
|
+
f"which was not successfully executed. "
|
|
247
|
+
f"If this action does not return any events, "
|
|
248
|
+
f"you do not need to do anything. "
|
|
249
|
+
f"If this is a custom action which returns events, "
|
|
250
|
+
f"you are recommended to implement this action "
|
|
251
|
+
f"in your action server and try again."
|
|
252
|
+
)
|
|
253
|
+
await _ask_questions(warning_questions, conversation_id, endpoint)
|
|
254
|
+
|
|
255
|
+
payload = ActionExecuted(action_name).as_dict()
|
|
256
|
+
return await send_event(endpoint, conversation_id, payload)
|
|
257
|
+
else:
|
|
258
|
+
logger.error("failed to execute action!")
|
|
259
|
+
raise
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def send_event(
|
|
263
|
+
endpoint: EndpointConfig,
|
|
264
|
+
conversation_id: Text,
|
|
265
|
+
evt: Union[List[Dict[Text, Any]], Dict[Text, Any]],
|
|
266
|
+
) -> Optional[Any]:
|
|
267
|
+
"""Log an event to a conversation."""
|
|
268
|
+
subpath = f"/conversations/{conversation_id}/tracker/events"
|
|
269
|
+
|
|
270
|
+
return await endpoint.request(json=evt, method="post", subpath=subpath)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def format_bot_output(message: BotUttered) -> Text:
|
|
274
|
+
"""Format a bot response to be displayed in the history table."""
|
|
275
|
+
# First, add text to output
|
|
276
|
+
output = message.text or ""
|
|
277
|
+
|
|
278
|
+
# Then, append all additional items
|
|
279
|
+
data = message.data or {}
|
|
280
|
+
if not data:
|
|
281
|
+
return output
|
|
282
|
+
|
|
283
|
+
if "image" in data and data["image"] is not None:
|
|
284
|
+
output += "\nImage: " + data["image"]
|
|
285
|
+
|
|
286
|
+
if "attachment" in data and data["attachment"] is not None:
|
|
287
|
+
output += "\nAttachment: " + data["attachment"]
|
|
288
|
+
|
|
289
|
+
if "buttons" in data and data["buttons"] is not None:
|
|
290
|
+
output += "\nButtons:"
|
|
291
|
+
choices = rasa.cli.utils.button_choices_from_message_data(
|
|
292
|
+
data, allow_free_text_input=True
|
|
293
|
+
)
|
|
294
|
+
for choice in choices:
|
|
295
|
+
output += "\n" + choice
|
|
296
|
+
|
|
297
|
+
if "elements" in data and data["elements"] is not None:
|
|
298
|
+
output += "\nElements:"
|
|
299
|
+
for idx, element in enumerate(data["elements"]):
|
|
300
|
+
element_str = rasa.cli.utils.element_to_string(element, idx)
|
|
301
|
+
output += "\n" + element_str
|
|
302
|
+
|
|
303
|
+
if "quick_replies" in data and data["quick_replies"] is not None:
|
|
304
|
+
output += "\nQuick replies:"
|
|
305
|
+
for idx, element in enumerate(data["quick_replies"]):
|
|
306
|
+
element_str = rasa.cli.utils.element_to_string(element, idx)
|
|
307
|
+
output += "\n" + element_str
|
|
308
|
+
return output
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def latest_user_message(events: List[Dict[Text, Any]]) -> Optional[Dict[Text, Any]]:
|
|
312
|
+
"""Return most recent user message."""
|
|
313
|
+
for i, e in enumerate(reversed(events)):
|
|
314
|
+
if e.get("event") == UserUttered.type_name:
|
|
315
|
+
return e
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
async def _ask_questions(
|
|
320
|
+
questions: Union[Form, Question],
|
|
321
|
+
conversation_id: Text,
|
|
322
|
+
endpoint: EndpointConfig,
|
|
323
|
+
is_abort: Callable[[Dict[Text, Any]], bool] = lambda x: False,
|
|
324
|
+
) -> Any:
|
|
325
|
+
"""Ask the user a question, if Ctrl-C is pressed provide user with menu."""
|
|
326
|
+
should_retry = True
|
|
327
|
+
answers: Any = {}
|
|
328
|
+
|
|
329
|
+
while should_retry:
|
|
330
|
+
answers = await questions.ask_async()
|
|
331
|
+
if answers is None or is_abort(answers):
|
|
332
|
+
should_retry = await _ask_if_quit(conversation_id, endpoint)
|
|
333
|
+
else:
|
|
334
|
+
should_retry = False
|
|
335
|
+
return answers
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _selection_choices_from_intent_prediction(
|
|
339
|
+
predictions: List[Dict[Text, Any]],
|
|
340
|
+
) -> List[Dict[Text, Any]]:
|
|
341
|
+
"""Given a list of ML predictions create a UI choice list."""
|
|
342
|
+
sorted_intents = sorted(
|
|
343
|
+
predictions, key=lambda k: (-k["confidence"], k[INTENT_NAME_KEY])
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
choices = []
|
|
347
|
+
for p in sorted_intents:
|
|
348
|
+
name_with_confidence = (
|
|
349
|
+
f'{p.get("confidence"):03.2f} {p.get(INTENT_NAME_KEY):40}'
|
|
350
|
+
)
|
|
351
|
+
choice = {
|
|
352
|
+
INTENT_NAME_KEY: name_with_confidence,
|
|
353
|
+
"value": p.get(INTENT_NAME_KEY),
|
|
354
|
+
}
|
|
355
|
+
choices.append(choice)
|
|
356
|
+
|
|
357
|
+
return choices
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def _request_free_text_intent(
|
|
361
|
+
conversation_id: Text, endpoint: EndpointConfig
|
|
362
|
+
) -> Text:
|
|
363
|
+
question = questionary.text(
|
|
364
|
+
message="Please type the intent name:",
|
|
365
|
+
validate=io_utils.not_empty_validator("Please enter an intent name"),
|
|
366
|
+
)
|
|
367
|
+
return await _ask_questions(question, conversation_id, endpoint)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
async def _request_free_text_action(
|
|
371
|
+
conversation_id: Text, endpoint: EndpointConfig
|
|
372
|
+
) -> Text:
|
|
373
|
+
question = questionary.text(
|
|
374
|
+
message="Please type the action name:",
|
|
375
|
+
validate=io_utils.not_empty_validator("Please enter an action name"),
|
|
376
|
+
)
|
|
377
|
+
return await _ask_questions(question, conversation_id, endpoint)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def _request_free_text_utterance(
|
|
381
|
+
conversation_id: Text, endpoint: EndpointConfig, action: Text
|
|
382
|
+
) -> Text:
|
|
383
|
+
question = questionary.text(
|
|
384
|
+
message=(f"Please type the message for your new bot response '{action}':"),
|
|
385
|
+
validate=io_utils.not_empty_validator("Please enter a response"),
|
|
386
|
+
)
|
|
387
|
+
return await _ask_questions(question, conversation_id, endpoint)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
async def _request_selection_from_intents(
|
|
391
|
+
intents: List[Dict[Text, Text]], conversation_id: Text, endpoint: EndpointConfig
|
|
392
|
+
) -> Text:
|
|
393
|
+
question = questionary.select("What intent is it?", choices=intents)
|
|
394
|
+
return await _ask_questions(question, conversation_id, endpoint)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def _request_fork_point_from_list(
|
|
398
|
+
forks: List[Dict[Text, Text]], conversation_id: Text, endpoint: EndpointConfig
|
|
399
|
+
) -> Text:
|
|
400
|
+
question = questionary.select(
|
|
401
|
+
"Before which user message do you want to fork?", choices=forks
|
|
402
|
+
)
|
|
403
|
+
return await _ask_questions(question, conversation_id, endpoint)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def _request_fork_from_user(
|
|
407
|
+
conversation_id: Text, endpoint: EndpointConfig
|
|
408
|
+
) -> Optional[List[Dict[Text, Any]]]:
|
|
409
|
+
"""Take in a conversation and ask at which point to fork the conversation.
|
|
410
|
+
|
|
411
|
+
Returns the list of events that should be kept. Forking means, the
|
|
412
|
+
conversation will be reset and continued from this previous point.
|
|
413
|
+
"""
|
|
414
|
+
tracker = await retrieve_tracker(
|
|
415
|
+
endpoint, conversation_id, EventVerbosity.AFTER_RESTART
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
choices = []
|
|
419
|
+
for i, e in enumerate(tracker.get("events", [])):
|
|
420
|
+
if e.get("event") == UserUttered.type_name:
|
|
421
|
+
choices.append({"name": e.get("text"), "value": i})
|
|
422
|
+
|
|
423
|
+
fork_idx = await _request_fork_point_from_list(
|
|
424
|
+
list(reversed(choices)), conversation_id, endpoint
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if fork_idx is not None:
|
|
428
|
+
return tracker.get("events", [])[: int(fork_idx)]
|
|
429
|
+
else:
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
async def _request_intent_from_user(
|
|
434
|
+
latest_message: Dict[Text, Any],
|
|
435
|
+
intents: List[Text],
|
|
436
|
+
conversation_id: Text,
|
|
437
|
+
endpoint: EndpointConfig,
|
|
438
|
+
) -> Dict[Text, Any]:
|
|
439
|
+
"""Take in latest message and ask which intent it should have been.
|
|
440
|
+
|
|
441
|
+
Returns the intent dict that has been selected by the user.
|
|
442
|
+
"""
|
|
443
|
+
predictions = latest_message.get("parse_data", {}).get("intent_ranking", [])
|
|
444
|
+
|
|
445
|
+
predicted_intents = {p[INTENT_NAME_KEY] for p in predictions}
|
|
446
|
+
|
|
447
|
+
for i in intents:
|
|
448
|
+
if i not in predicted_intents:
|
|
449
|
+
predictions.append({INTENT_NAME_KEY: i, "confidence": 0.0})
|
|
450
|
+
|
|
451
|
+
# convert intents to ui list and add <other> as a free text alternative
|
|
452
|
+
choices = [
|
|
453
|
+
{INTENT_NAME_KEY: "<create_new_intent>", "value": OTHER_INTENT}
|
|
454
|
+
] + _selection_choices_from_intent_prediction(predictions)
|
|
455
|
+
|
|
456
|
+
intent_name = await _request_selection_from_intents(
|
|
457
|
+
choices, conversation_id, endpoint
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if intent_name == OTHER_INTENT:
|
|
461
|
+
intent_name = await _request_free_text_intent(conversation_id, endpoint)
|
|
462
|
+
selected_intent = {INTENT_NAME_KEY: intent_name, "confidence": 1.0}
|
|
463
|
+
else:
|
|
464
|
+
# returns the selected intent with the original probability value
|
|
465
|
+
selected_intent = next(
|
|
466
|
+
(x for x in predictions if x[INTENT_NAME_KEY] == intent_name),
|
|
467
|
+
{INTENT_NAME_KEY: None},
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return selected_intent
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
async def _print_history(conversation_id: Text, endpoint: EndpointConfig) -> None:
|
|
474
|
+
"""Print information about the conversation for the user."""
|
|
475
|
+
tracker_dump = await retrieve_tracker(
|
|
476
|
+
endpoint, conversation_id, EventVerbosity.AFTER_RESTART
|
|
477
|
+
)
|
|
478
|
+
events = tracker_dump.get("events", [])
|
|
479
|
+
|
|
480
|
+
table = _chat_history_table(events)
|
|
481
|
+
slot_strings = _slot_history(tracker_dump)
|
|
482
|
+
|
|
483
|
+
print("------")
|
|
484
|
+
print("Chat History\n")
|
|
485
|
+
loop = asyncio.get_running_loop()
|
|
486
|
+
loop.run_in_executor(None, print, table)
|
|
487
|
+
|
|
488
|
+
if slot_strings:
|
|
489
|
+
print("\n")
|
|
490
|
+
slots_info = f"Current slots: \n\t{', '.join(slot_strings)}\n"
|
|
491
|
+
loop.run_in_executor(None, print, slots_info)
|
|
492
|
+
|
|
493
|
+
loop.run_in_executor(None, print, "------")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _chat_history_table(events: List[Dict[Text, Any]]) -> Text:
|
|
497
|
+
"""Create a table containing bot and user messages.
|
|
498
|
+
|
|
499
|
+
Also includes additional information, like any events and
|
|
500
|
+
prediction probabilities.
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
def wrap(txt: Text, max_width: int) -> Text:
|
|
504
|
+
true_wrapping_width = calc_true_wrapping_width(txt, max_width)
|
|
505
|
+
return "\n".join(
|
|
506
|
+
textwrap.wrap(txt, true_wrapping_width, replace_whitespace=False)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def colored(txt: Text, color: Text) -> Text:
|
|
510
|
+
return "{" + color + "}" + txt + "{/" + color + "}"
|
|
511
|
+
|
|
512
|
+
def format_user_msg(user_event: UserUttered, max_width: int) -> Text:
|
|
513
|
+
intent = user_event.intent or {}
|
|
514
|
+
intent_name = intent.get(INTENT_NAME_KEY, "")
|
|
515
|
+
_confidence = intent.get("confidence", 1.0)
|
|
516
|
+
_md = _as_md_message(user_event.parse_data)
|
|
517
|
+
|
|
518
|
+
_lines = [
|
|
519
|
+
colored(wrap(_md, max_width), "hired"),
|
|
520
|
+
f"intent: {intent_name} {_confidence:03.2f}",
|
|
521
|
+
]
|
|
522
|
+
return "\n".join(_lines)
|
|
523
|
+
|
|
524
|
+
def bot_width(_table: AsciiTable) -> int:
|
|
525
|
+
return _table.column_max_width(1)
|
|
526
|
+
|
|
527
|
+
def user_width(_table: AsciiTable) -> int:
|
|
528
|
+
return _table.column_max_width(3)
|
|
529
|
+
|
|
530
|
+
def add_bot_cell(data: List[List[Union[Text, Color]]], cell: Text) -> None:
|
|
531
|
+
data.append([len(data), Color(cell), "", ""])
|
|
532
|
+
|
|
533
|
+
def add_user_cell(data: List[List[Union[Text, Color]]], cell: Text) -> None:
|
|
534
|
+
data.append([len(data), "", "", Color(cell)])
|
|
535
|
+
|
|
536
|
+
# prints the historical interactions between the bot and the user,
|
|
537
|
+
# to help with correctly identifying the action
|
|
538
|
+
table_data = [
|
|
539
|
+
[
|
|
540
|
+
"# ",
|
|
541
|
+
Color(colored("Bot ", "autoblue")),
|
|
542
|
+
" ",
|
|
543
|
+
Color(colored("You ", "hired")),
|
|
544
|
+
]
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
table = SingleTable(table_data, "Chat History")
|
|
548
|
+
|
|
549
|
+
bot_column = []
|
|
550
|
+
|
|
551
|
+
tracker = DialogueStateTracker.from_dict("any", events)
|
|
552
|
+
applied_events = tracker.applied_events()
|
|
553
|
+
|
|
554
|
+
for idx, event in enumerate(applied_events):
|
|
555
|
+
if isinstance(event, ActionExecuted):
|
|
556
|
+
if (
|
|
557
|
+
event.action_name == ACTION_UNLIKELY_INTENT_NAME
|
|
558
|
+
and event.confidence == 0
|
|
559
|
+
):
|
|
560
|
+
continue
|
|
561
|
+
bot_column.append(colored(str(event), "autocyan"))
|
|
562
|
+
if event.confidence is not None:
|
|
563
|
+
bot_column[-1] += colored(f" {event.confidence:03.2f}", "autowhite")
|
|
564
|
+
|
|
565
|
+
elif isinstance(event, UserUttered):
|
|
566
|
+
if bot_column:
|
|
567
|
+
text = "\n".join(bot_column)
|
|
568
|
+
add_bot_cell(table_data, text)
|
|
569
|
+
bot_column = []
|
|
570
|
+
|
|
571
|
+
msg = format_user_msg(event, user_width(table))
|
|
572
|
+
add_user_cell(table_data, msg)
|
|
573
|
+
|
|
574
|
+
elif isinstance(event, BotUttered):
|
|
575
|
+
wrapped = wrap(format_bot_output(event), bot_width(table))
|
|
576
|
+
bot_column.append(colored(wrapped, "autoblue"))
|
|
577
|
+
|
|
578
|
+
else:
|
|
579
|
+
if event.as_story_string():
|
|
580
|
+
bot_column.append(wrap(event.as_story_string(), bot_width(table)))
|
|
581
|
+
|
|
582
|
+
if bot_column:
|
|
583
|
+
text = "\n".join(bot_column)
|
|
584
|
+
add_bot_cell(table_data, text)
|
|
585
|
+
|
|
586
|
+
table.inner_heading_row_border = False
|
|
587
|
+
table.inner_row_border = True
|
|
588
|
+
table.inner_column_border = False
|
|
589
|
+
table.outer_border = False
|
|
590
|
+
table.justify_columns = {0: "left", 1: "left", 2: "center", 3: "right"}
|
|
591
|
+
|
|
592
|
+
return table.table
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _slot_history(tracker_dump: Dict[Text, Any]) -> List[Text]:
|
|
596
|
+
"""Create an array of slot representations to be displayed."""
|
|
597
|
+
slot_strings = []
|
|
598
|
+
for k, s in tracker_dump.get("slots", {}).items():
|
|
599
|
+
colored_value = rasa.shared.utils.io.wrap_with_color(
|
|
600
|
+
str(s), color=rasa.shared.utils.io.bcolors.WARNING
|
|
601
|
+
)
|
|
602
|
+
slot_strings.append(f"{k}: {colored_value}")
|
|
603
|
+
return slot_strings
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
async def _retry_on_error(
|
|
607
|
+
func: Callable, export_path: Text, *args: Any, **kwargs: Any
|
|
608
|
+
) -> None:
|
|
609
|
+
while True:
|
|
610
|
+
try:
|
|
611
|
+
return func(export_path, *args, **kwargs)
|
|
612
|
+
except OSError as e:
|
|
613
|
+
answer = await questionary.confirm(
|
|
614
|
+
f"Failed to export '{export_path}': {e}. Please make sure 'rasa' "
|
|
615
|
+
f"has read and write access to this file. Would you like to retry?"
|
|
616
|
+
).ask_async()
|
|
617
|
+
if not answer:
|
|
618
|
+
raise e
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
async def _write_data_to_file(conversation_id: Text, endpoint: EndpointConfig) -> None:
|
|
622
|
+
"""Write stories and nlu data to file."""
|
|
623
|
+
story_path, nlu_path, domain_path = await _request_export_info()
|
|
624
|
+
|
|
625
|
+
tracker = await retrieve_tracker(endpoint, conversation_id)
|
|
626
|
+
events = tracker.get("events", [])
|
|
627
|
+
|
|
628
|
+
serialised_domain = await retrieve_domain(endpoint)
|
|
629
|
+
domain = Domain.from_dict(serialised_domain)
|
|
630
|
+
|
|
631
|
+
await _retry_on_error(_write_stories_to_file, story_path, events, domain)
|
|
632
|
+
await _retry_on_error(_write_nlu_to_file, nlu_path, events)
|
|
633
|
+
await _retry_on_error(_write_domain_to_file, domain_path, events, domain)
|
|
634
|
+
|
|
635
|
+
logger.info("Successfully wrote stories and NLU data")
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
async def _ask_if_quit(conversation_id: Text, endpoint: EndpointConfig) -> bool:
|
|
639
|
+
"""Display the exit menu.
|
|
640
|
+
|
|
641
|
+
Return `True` if the previous question should be retried.
|
|
642
|
+
"""
|
|
643
|
+
answer = await questionary.select(
|
|
644
|
+
message="Do you want to stop?",
|
|
645
|
+
choices=[
|
|
646
|
+
Choice("Continue", "continue"),
|
|
647
|
+
Choice("Undo Last", "undo"),
|
|
648
|
+
Choice("Fork", "fork"),
|
|
649
|
+
Choice("Start Fresh", "restart"),
|
|
650
|
+
Choice("Export & Quit", "quit"),
|
|
651
|
+
],
|
|
652
|
+
).ask_async()
|
|
653
|
+
|
|
654
|
+
if not answer or answer == "quit":
|
|
655
|
+
# this is also the default answer if the user presses Ctrl-C
|
|
656
|
+
await _write_data_to_file(conversation_id, endpoint)
|
|
657
|
+
raise Abort()
|
|
658
|
+
elif answer == "undo":
|
|
659
|
+
raise UndoLastStep()
|
|
660
|
+
elif answer == "fork":
|
|
661
|
+
raise ForkTracker()
|
|
662
|
+
elif answer == "restart":
|
|
663
|
+
raise RestartConversation()
|
|
664
|
+
else: # `continue` or no answer
|
|
665
|
+
# in this case we will just return, and the original
|
|
666
|
+
# question will get asked again
|
|
667
|
+
return True
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
async def _request_action_from_user(
|
|
671
|
+
predictions: List[Dict[Text, Any]], conversation_id: Text, endpoint: EndpointConfig
|
|
672
|
+
) -> Tuple[Text, bool]:
|
|
673
|
+
"""Ask the user to correct an action prediction."""
|
|
674
|
+
await _print_history(conversation_id, endpoint)
|
|
675
|
+
|
|
676
|
+
choices = [
|
|
677
|
+
{"name": f'{a["score"]:03.2f} {a["action"]:40}', "value": a["action"]}
|
|
678
|
+
for a in predictions
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
tracker = await retrieve_tracker(endpoint, conversation_id)
|
|
682
|
+
events = tracker.get("events", [])
|
|
683
|
+
|
|
684
|
+
session_actions_all = [a["name"] for a in _collect_actions(events)]
|
|
685
|
+
session_actions_unique = list(set(session_actions_all))
|
|
686
|
+
old_actions = [action["value"] for action in choices]
|
|
687
|
+
new_actions = [
|
|
688
|
+
{"name": action, "value": OTHER_ACTION + action}
|
|
689
|
+
for action in session_actions_unique
|
|
690
|
+
if action not in old_actions
|
|
691
|
+
]
|
|
692
|
+
choices = (
|
|
693
|
+
[{"name": "<create new action>", "value": NEW_ACTION}] + new_actions + choices
|
|
694
|
+
)
|
|
695
|
+
question = questionary.select("What is the next action of the bot?", choices)
|
|
696
|
+
|
|
697
|
+
action_name = await _ask_questions(question, conversation_id, endpoint)
|
|
698
|
+
is_new_action = action_name == NEW_ACTION
|
|
699
|
+
|
|
700
|
+
if is_new_action:
|
|
701
|
+
# create new action
|
|
702
|
+
action_name = await _request_free_text_action(conversation_id, endpoint)
|
|
703
|
+
if action_name.startswith(UTTER_PREFIX):
|
|
704
|
+
utter_message = await _request_free_text_utterance(
|
|
705
|
+
conversation_id, endpoint, action_name
|
|
706
|
+
)
|
|
707
|
+
NEW_RESPONSES[action_name] = [{KEY_RESPONSES_TEXT: utter_message}]
|
|
708
|
+
|
|
709
|
+
elif action_name[:32] == OTHER_ACTION:
|
|
710
|
+
# action was newly created in the session, but not this turn
|
|
711
|
+
is_new_action = True
|
|
712
|
+
action_name = action_name[32:]
|
|
713
|
+
|
|
714
|
+
print(f"Thanks! The bot will now run {action_name}.\n")
|
|
715
|
+
return action_name, is_new_action
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
async def _request_export_info() -> Tuple[Text, Text, Text]:
|
|
719
|
+
import rasa.shared.data
|
|
720
|
+
|
|
721
|
+
"""Request file path and export stories & nlu data to that path"""
|
|
722
|
+
|
|
723
|
+
# export training data and quit
|
|
724
|
+
questions = questionary.form(
|
|
725
|
+
export_stories=questionary.text(
|
|
726
|
+
message="Export stories to (if file exists, this "
|
|
727
|
+
"will append the stories)",
|
|
728
|
+
default=PATHS["stories"],
|
|
729
|
+
validate=io_utils.file_type_validator(
|
|
730
|
+
rasa.shared.data.YAML_FILE_EXTENSIONS,
|
|
731
|
+
"Please provide a valid export path for the stories, "
|
|
732
|
+
"e.g. 'stories.yml'.",
|
|
733
|
+
),
|
|
734
|
+
),
|
|
735
|
+
export_nlu=questionary.text(
|
|
736
|
+
message="Export NLU data to (if file exists, this will "
|
|
737
|
+
"merge learned data with previous training examples)",
|
|
738
|
+
default=PATHS["nlu"],
|
|
739
|
+
validate=io_utils.file_type_validator(
|
|
740
|
+
list(rasa.shared.data.TRAINING_DATA_EXTENSIONS),
|
|
741
|
+
"Please provide a valid export path for the NLU data, "
|
|
742
|
+
"e.g. 'nlu.yml'.",
|
|
743
|
+
),
|
|
744
|
+
),
|
|
745
|
+
export_domain=questionary.text(
|
|
746
|
+
message="Export domain file to (if file exists, this "
|
|
747
|
+
"will be overwritten)",
|
|
748
|
+
default=PATHS["domain"],
|
|
749
|
+
validate=io_utils.file_type_validator(
|
|
750
|
+
rasa.shared.data.YAML_FILE_EXTENSIONS,
|
|
751
|
+
"Please provide a valid export path for the domain file, "
|
|
752
|
+
"e.g. 'domain.yml'.",
|
|
753
|
+
),
|
|
754
|
+
),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
answers = await questions.ask_async()
|
|
758
|
+
if not answers:
|
|
759
|
+
raise Abort()
|
|
760
|
+
|
|
761
|
+
return answers["export_stories"], answers["export_nlu"], answers["export_domain"]
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _split_conversation_at_restarts(
|
|
765
|
+
events: List[Dict[Text, Any]],
|
|
766
|
+
) -> List[List[Dict[Text, Any]]]:
|
|
767
|
+
"""Split a conversation at restart events.
|
|
768
|
+
|
|
769
|
+
Returns an array of event lists, without the restart events.
|
|
770
|
+
"""
|
|
771
|
+
deserialized_events = [Event.from_parameters(event) for event in events]
|
|
772
|
+
split_events = rasa.shared.core.events.split_events(
|
|
773
|
+
deserialized_events, Restarted, include_splitting_event=False
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
return [[event.as_dict() for event in events] for events in split_events]
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _collect_messages(events: List[Dict[Text, Any]]) -> List[Message]:
|
|
780
|
+
"""Collect the message text and parsed data from the UserMessage events
|
|
781
|
+
into a list.
|
|
782
|
+
"""
|
|
783
|
+
import rasa.shared.nlu.training_data.util as rasa_nlu_training_data_utils
|
|
784
|
+
|
|
785
|
+
messages = []
|
|
786
|
+
|
|
787
|
+
for event in events:
|
|
788
|
+
if event.get("event") == UserUttered.type_name:
|
|
789
|
+
data = event.get("parse_data", {})
|
|
790
|
+
rasa_nlu_training_data_utils.remove_untrainable_entities_from(data)
|
|
791
|
+
msg = Message.build(
|
|
792
|
+
data["text"], data["intent"][INTENT_NAME_KEY], data["entities"]
|
|
793
|
+
)
|
|
794
|
+
messages.append(msg)
|
|
795
|
+
elif event.get("event") == UserUtteranceReverted.type_name and messages:
|
|
796
|
+
messages.pop() # user corrected the nlu, remove incorrect example
|
|
797
|
+
|
|
798
|
+
return messages
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _collect_actions(events: List[Dict[Text, Any]]) -> List[Dict[Text, Any]]:
|
|
802
|
+
"""Collect all the `ActionExecuted` events into a list."""
|
|
803
|
+
return [evt for evt in events if evt.get("event") == ActionExecuted.type_name]
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _write_stories_to_file(
|
|
807
|
+
export_story_path: Text, events: List[Dict[Text, Any]], domain: Domain
|
|
808
|
+
) -> None:
|
|
809
|
+
"""Write the conversation of the conversation_id to the file paths."""
|
|
810
|
+
from rasa.shared.core.training_data.story_writer.yaml_story_writer import (
|
|
811
|
+
YAMLStoryWriter,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
sub_conversations = _split_conversation_at_restarts(events)
|
|
815
|
+
io_utils.create_path(export_story_path)
|
|
816
|
+
|
|
817
|
+
if rasa.shared.data.is_likely_yaml_file(export_story_path):
|
|
818
|
+
writer = YAMLStoryWriter()
|
|
819
|
+
|
|
820
|
+
should_append_stories = False
|
|
821
|
+
if os.path.exists(export_story_path):
|
|
822
|
+
append_write = "a" # append if already exists
|
|
823
|
+
should_append_stories = True
|
|
824
|
+
else:
|
|
825
|
+
append_write = "w" # make a new file if not
|
|
826
|
+
|
|
827
|
+
with open(
|
|
828
|
+
export_story_path, append_write, encoding=rasa.shared.utils.io.DEFAULT_ENCODING
|
|
829
|
+
) as f:
|
|
830
|
+
interactive_story_counter = 1
|
|
831
|
+
for conversation in sub_conversations:
|
|
832
|
+
parsed_events = rasa.shared.core.events.deserialise_events(conversation)
|
|
833
|
+
tracker = DialogueStateTracker.from_events(
|
|
834
|
+
f"interactive_story_{interactive_story_counter}",
|
|
835
|
+
evts=parsed_events,
|
|
836
|
+
slots=domain.slots,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
if any(
|
|
840
|
+
isinstance(event, UserUttered) for event in tracker.applied_events()
|
|
841
|
+
):
|
|
842
|
+
interactive_story_counter += 1
|
|
843
|
+
f.write(
|
|
844
|
+
"\n"
|
|
845
|
+
+ tracker.export_stories(
|
|
846
|
+
writer=writer,
|
|
847
|
+
should_append_stories=should_append_stories,
|
|
848
|
+
e2e=SAVE_IN_E2E,
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _filter_messages(msgs: List[Message]) -> List[Message]:
|
|
854
|
+
"""Filter messages removing those that start with INTENT_MESSAGE_PREFIX."""
|
|
855
|
+
filtered_messages = []
|
|
856
|
+
for msg in msgs:
|
|
857
|
+
if not msg.get(TEXT).startswith(INTENT_MESSAGE_PREFIX):
|
|
858
|
+
filtered_messages.append(msg)
|
|
859
|
+
return filtered_messages
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _write_nlu_to_file(export_nlu_path: Text, events: List[Dict[Text, Any]]) -> None:
|
|
863
|
+
"""Write the nlu data of the conversation_id to the file paths."""
|
|
864
|
+
from rasa.shared.nlu.training_data.training_data import TrainingData
|
|
865
|
+
|
|
866
|
+
msgs = _collect_messages(events)
|
|
867
|
+
msgs = _filter_messages(msgs)
|
|
868
|
+
|
|
869
|
+
# noinspection PyBroadException
|
|
870
|
+
try:
|
|
871
|
+
previous_examples = loading.load_data(export_nlu_path)
|
|
872
|
+
except Exception as e:
|
|
873
|
+
logger.debug(f"An exception occurred while trying to load the NLU data. {e!s}")
|
|
874
|
+
# No previous file exists, use empty training data as replacement.
|
|
875
|
+
previous_examples = TrainingData()
|
|
876
|
+
|
|
877
|
+
nlu_data = previous_examples.merge(TrainingData(msgs))
|
|
878
|
+
|
|
879
|
+
# need to guess the format of the file before opening it to avoid a read
|
|
880
|
+
# in a write
|
|
881
|
+
nlu_format = _get_nlu_target_format(export_nlu_path)
|
|
882
|
+
if nlu_format == RASA_YAML:
|
|
883
|
+
stringified_training_data = nlu_data.nlu_as_yaml()
|
|
884
|
+
else:
|
|
885
|
+
stringified_training_data = nlu_data.nlu_as_json()
|
|
886
|
+
|
|
887
|
+
rasa.shared.utils.io.write_text_file(stringified_training_data, export_nlu_path)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _get_nlu_target_format(export_path: Text) -> Text:
|
|
891
|
+
guessed_format = loading.guess_format(export_path)
|
|
892
|
+
|
|
893
|
+
if guessed_format not in {RASA, RASA_YAML}:
|
|
894
|
+
if rasa.shared.data.is_likely_json_file(export_path):
|
|
895
|
+
guessed_format = RASA
|
|
896
|
+
elif rasa.shared.data.is_likely_yaml_file(export_path):
|
|
897
|
+
guessed_format = RASA_YAML
|
|
898
|
+
|
|
899
|
+
return guessed_format
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _entities_from_messages(messages: List[Message]) -> List[Text]:
|
|
903
|
+
"""Return all entities that occur in at least one of the messages."""
|
|
904
|
+
return list({e["entity"] for m in messages for e in m.data.get("entities", [])})
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _intents_from_messages(messages: List[Message]) -> Set[Text]:
|
|
908
|
+
"""Return all intents that occur in at least one of the messages."""
|
|
909
|
+
# set of distinct intents
|
|
910
|
+
distinct_intents = {m.data["intent"] for m in messages if "intent" in m.data}
|
|
911
|
+
|
|
912
|
+
return distinct_intents
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _write_domain_to_file(
|
|
916
|
+
domain_path: Text, events: List[Dict[Text, Any]], old_domain: Domain
|
|
917
|
+
) -> None:
|
|
918
|
+
"""Write an updated domain file to the file path."""
|
|
919
|
+
io_utils.create_path(domain_path)
|
|
920
|
+
|
|
921
|
+
messages = _collect_messages(events)
|
|
922
|
+
actions = _collect_actions(events)
|
|
923
|
+
responses = NEW_RESPONSES
|
|
924
|
+
|
|
925
|
+
# TODO for now there is no way to distinguish between action and form
|
|
926
|
+
collected_actions = list(
|
|
927
|
+
{
|
|
928
|
+
e["name"]
|
|
929
|
+
for e in actions
|
|
930
|
+
if e["name"] not in rasa.shared.core.constants.DEFAULT_ACTION_NAMES
|
|
931
|
+
and e["name"] not in old_domain.form_names
|
|
932
|
+
}
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
new_domain = Domain.from_dict(
|
|
936
|
+
{
|
|
937
|
+
KEY_INTENTS: list(_intents_from_messages(messages)),
|
|
938
|
+
KEY_ENTITIES: _entities_from_messages(messages),
|
|
939
|
+
KEY_RESPONSES: responses,
|
|
940
|
+
KEY_ACTIONS: collected_actions,
|
|
941
|
+
}
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
old_domain.merge(new_domain).persist(domain_path)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
async def _predict_till_next_listen(
|
|
948
|
+
endpoint: EndpointConfig,
|
|
949
|
+
conversation_id: Text,
|
|
950
|
+
conversation_ids: List[Text],
|
|
951
|
+
plot_file: Optional[Text],
|
|
952
|
+
) -> None:
|
|
953
|
+
"""Predict and validate actions until we need to wait for a user message."""
|
|
954
|
+
listen = False
|
|
955
|
+
while not listen:
|
|
956
|
+
result = await request_prediction(endpoint, conversation_id)
|
|
957
|
+
if result is None:
|
|
958
|
+
result = {}
|
|
959
|
+
|
|
960
|
+
predictions = result.get("scores", [])
|
|
961
|
+
if not predictions:
|
|
962
|
+
raise InvalidConfigException(
|
|
963
|
+
"Cannot continue as no action was predicted by the dialogue manager. "
|
|
964
|
+
"This can happen if you trained the assistant with no policy included "
|
|
965
|
+
"in the configuration. If so, please re-train the assistant with at "
|
|
966
|
+
f"least one policy ({DOCS_URL_NLU_BASED_POLICIES}) "
|
|
967
|
+
"included in the configuration."
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
probabilities = [prediction["score"] for prediction in predictions]
|
|
971
|
+
pred_out = int(np.argmax(probabilities))
|
|
972
|
+
action_name = predictions[pred_out].get("action")
|
|
973
|
+
policy = result.get("policy")
|
|
974
|
+
confidence = result.get("confidence")
|
|
975
|
+
|
|
976
|
+
await _print_history(conversation_id, endpoint)
|
|
977
|
+
await _plot_trackers(
|
|
978
|
+
conversation_ids,
|
|
979
|
+
plot_file,
|
|
980
|
+
endpoint,
|
|
981
|
+
unconfirmed=[ActionExecuted(action_name)],
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
listen = await _validate_action(
|
|
985
|
+
action_name, policy, confidence, predictions, endpoint, conversation_id
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
await _plot_trackers(conversation_ids, plot_file, endpoint)
|
|
989
|
+
|
|
990
|
+
tracker_dump = await retrieve_tracker(
|
|
991
|
+
endpoint, conversation_id, EventVerbosity.AFTER_RESTART
|
|
992
|
+
)
|
|
993
|
+
events = tracker_dump.get("events", [])
|
|
994
|
+
|
|
995
|
+
if len(events) >= 2:
|
|
996
|
+
last_event = events[-2] # last event before action_listen
|
|
997
|
+
|
|
998
|
+
# if bot message includes buttons the user will get a list choice to reply
|
|
999
|
+
# the list choice is displayed in place of action listen
|
|
1000
|
+
if last_event.get("event") == BotUttered.type_name and last_event["data"].get(
|
|
1001
|
+
"buttons", None
|
|
1002
|
+
):
|
|
1003
|
+
user_selection = await _get_button_choice(last_event)
|
|
1004
|
+
if user_selection != rasa.cli.utils.FREE_TEXT_INPUT_PROMPT:
|
|
1005
|
+
await send_message(endpoint, conversation_id, user_selection)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
async def _get_button_choice(last_event: Dict[Text, Any]) -> Text:
|
|
1009
|
+
data = last_event["data"]
|
|
1010
|
+
message = last_event.get("text", "")
|
|
1011
|
+
|
|
1012
|
+
choices = rasa.cli.utils.button_choices_from_message_data(
|
|
1013
|
+
data, allow_free_text_input=True
|
|
1014
|
+
)
|
|
1015
|
+
question = questionary.select(message, choices)
|
|
1016
|
+
return await rasa.cli.utils.payload_from_button_question(question)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
async def _correct_wrong_nlu(
|
|
1020
|
+
corrected_nlu: Dict[Text, Any],
|
|
1021
|
+
events: List[Dict[Text, Any]],
|
|
1022
|
+
endpoint: EndpointConfig,
|
|
1023
|
+
conversation_id: Text,
|
|
1024
|
+
) -> None:
|
|
1025
|
+
"""A wrong NLU prediction got corrected, update core's tracker."""
|
|
1026
|
+
revert_latest_user_utterance = UserUtteranceReverted().as_dict()
|
|
1027
|
+
# `UserUtteranceReverted` also removes the `ACTION_LISTEN` event before, hence we
|
|
1028
|
+
# have to replay it.
|
|
1029
|
+
listen_for_next_message = ActionExecuted(ACTION_LISTEN_NAME).as_dict()
|
|
1030
|
+
corrected_message = latest_user_message(events)
|
|
1031
|
+
|
|
1032
|
+
if corrected_message is None:
|
|
1033
|
+
raise Exception("Failed to correct NLU data. User message not found.")
|
|
1034
|
+
|
|
1035
|
+
corrected_message["parse_data"] = corrected_nlu
|
|
1036
|
+
await send_event(
|
|
1037
|
+
endpoint,
|
|
1038
|
+
conversation_id,
|
|
1039
|
+
[revert_latest_user_utterance, listen_for_next_message, corrected_message],
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
async def _correct_wrong_action(
|
|
1044
|
+
corrected_action: Text,
|
|
1045
|
+
endpoint: EndpointConfig,
|
|
1046
|
+
conversation_id: Text,
|
|
1047
|
+
is_new_action: bool = False,
|
|
1048
|
+
) -> None:
|
|
1049
|
+
"""A wrong action prediction got corrected, update core's tracker."""
|
|
1050
|
+
await send_action(
|
|
1051
|
+
endpoint, conversation_id, corrected_action, is_new_action=is_new_action
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _form_is_rejected(action_name: Text, tracker: Dict[Text, Any]) -> bool:
|
|
1056
|
+
"""Check if the form got rejected with the most recent action name."""
|
|
1057
|
+
return (
|
|
1058
|
+
tracker.get(ACTIVE_LOOP, {}).get(LOOP_NAME)
|
|
1059
|
+
and action_name != tracker[ACTIVE_LOOP][LOOP_NAME]
|
|
1060
|
+
and action_name != ACTION_LISTEN_NAME
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _form_is_restored(action_name: Text, tracker: Dict[Text, Any]) -> bool:
|
|
1065
|
+
"""Check whether the form is called again after it was rejected."""
|
|
1066
|
+
return (
|
|
1067
|
+
tracker.get(ACTIVE_LOOP, {}).get(LOOP_REJECTED)
|
|
1068
|
+
and tracker.get("latest_action_name") == ACTION_LISTEN_NAME
|
|
1069
|
+
and action_name == tracker.get(ACTIVE_LOOP, {}).get(LOOP_NAME)
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
async def _confirm_form_validation(
|
|
1074
|
+
action_name: Text,
|
|
1075
|
+
tracker: Dict[Text, Any],
|
|
1076
|
+
endpoint: EndpointConfig,
|
|
1077
|
+
conversation_id: Text,
|
|
1078
|
+
) -> None:
|
|
1079
|
+
"""Ask a user whether an input for a form should be validated.
|
|
1080
|
+
|
|
1081
|
+
Previous to this call, the active form was chosen after it was rejected.
|
|
1082
|
+
"""
|
|
1083
|
+
requested_slot = tracker.get("slots", {}).get(REQUESTED_SLOT)
|
|
1084
|
+
|
|
1085
|
+
validation_questions = questionary.confirm(
|
|
1086
|
+
f"Should '{action_name}' validate user input to fill "
|
|
1087
|
+
f"the slot '{requested_slot}'?"
|
|
1088
|
+
)
|
|
1089
|
+
validate_input = await _ask_questions(
|
|
1090
|
+
validation_questions, conversation_id, endpoint
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
if not validate_input:
|
|
1094
|
+
# notify form action to skip validation
|
|
1095
|
+
await send_event(
|
|
1096
|
+
endpoint,
|
|
1097
|
+
conversation_id,
|
|
1098
|
+
{
|
|
1099
|
+
"event": rasa.shared.core.events.LoopInterrupted.type_name,
|
|
1100
|
+
LOOP_INTERRUPTED: True,
|
|
1101
|
+
},
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
elif tracker.get(ACTIVE_LOOP, {}).get(LOOP_INTERRUPTED):
|
|
1105
|
+
# handle contradiction with learned behaviour
|
|
1106
|
+
warning_question = questionary.confirm(
|
|
1107
|
+
"ERROR: FormPolicy predicted no form validation "
|
|
1108
|
+
"based on previous training stories. "
|
|
1109
|
+
"Make sure to remove contradictory stories "
|
|
1110
|
+
"from training data. "
|
|
1111
|
+
"Otherwise predicting no form validation "
|
|
1112
|
+
"will not work as expected."
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
await _ask_questions(warning_question, conversation_id, endpoint)
|
|
1116
|
+
# notify form action to validate an input
|
|
1117
|
+
await send_event(
|
|
1118
|
+
endpoint,
|
|
1119
|
+
conversation_id,
|
|
1120
|
+
{
|
|
1121
|
+
"event": rasa.shared.core.events.LoopInterrupted.type_name,
|
|
1122
|
+
LOOP_INTERRUPTED: False,
|
|
1123
|
+
},
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
async def _validate_action(
|
|
1128
|
+
action_name: Text,
|
|
1129
|
+
policy: Text,
|
|
1130
|
+
confidence: float,
|
|
1131
|
+
predictions: List[Dict[Text, Any]],
|
|
1132
|
+
endpoint: EndpointConfig,
|
|
1133
|
+
conversation_id: Text,
|
|
1134
|
+
) -> bool:
|
|
1135
|
+
"""Query the user to validate if an action prediction is correct.
|
|
1136
|
+
|
|
1137
|
+
Returns `True` if the prediction is correct, `False` otherwise.
|
|
1138
|
+
"""
|
|
1139
|
+
if action_name == ACTION_UNLIKELY_INTENT_NAME:
|
|
1140
|
+
question = questionary.confirm(
|
|
1141
|
+
f"The bot wants to run '{action_name}' "
|
|
1142
|
+
f"to indicate that the last user message was unexpected "
|
|
1143
|
+
f"at this point in the conversation. "
|
|
1144
|
+
f"Check out UnexpecTEDIntentPolicy "
|
|
1145
|
+
f"({DOCS_URL_NLU_BASED_POLICIES}#unexpected-intent-policy) "
|
|
1146
|
+
f"to learn more. Is that correct?"
|
|
1147
|
+
)
|
|
1148
|
+
else:
|
|
1149
|
+
question = questionary.confirm(
|
|
1150
|
+
f"The bot wants to run '{action_name}', correct?"
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
is_correct = await _ask_questions(question, conversation_id, endpoint)
|
|
1154
|
+
|
|
1155
|
+
if not is_correct and action_name != ACTION_UNLIKELY_INTENT_NAME:
|
|
1156
|
+
action_name, is_new_action = await _request_action_from_user(
|
|
1157
|
+
predictions, conversation_id, endpoint
|
|
1158
|
+
)
|
|
1159
|
+
else:
|
|
1160
|
+
is_new_action = False
|
|
1161
|
+
|
|
1162
|
+
tracker = await retrieve_tracker(
|
|
1163
|
+
endpoint, conversation_id, EventVerbosity.AFTER_RESTART
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
if _form_is_rejected(action_name, tracker):
|
|
1167
|
+
# notify the tracker that form was rejected
|
|
1168
|
+
await send_event(
|
|
1169
|
+
endpoint,
|
|
1170
|
+
conversation_id,
|
|
1171
|
+
{
|
|
1172
|
+
"event": "action_execution_rejected",
|
|
1173
|
+
LOOP_NAME: tracker[ACTIVE_LOOP][LOOP_NAME],
|
|
1174
|
+
},
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
elif _form_is_restored(action_name, tracker):
|
|
1178
|
+
await _confirm_form_validation(action_name, tracker, endpoint, conversation_id)
|
|
1179
|
+
|
|
1180
|
+
if not is_correct:
|
|
1181
|
+
await _correct_wrong_action(
|
|
1182
|
+
action_name, endpoint, conversation_id, is_new_action=is_new_action
|
|
1183
|
+
)
|
|
1184
|
+
else:
|
|
1185
|
+
await send_action(endpoint, conversation_id, action_name, policy, confidence)
|
|
1186
|
+
|
|
1187
|
+
return action_name == ACTION_LISTEN_NAME
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _as_md_message(parse_data: Dict[Text, Any]) -> Text:
|
|
1191
|
+
"""Display the parse data of a message in markdown format."""
|
|
1192
|
+
from rasa.shared.nlu.training_data.formats.readerwriter import TrainingDataWriter
|
|
1193
|
+
|
|
1194
|
+
if parse_data.get("text", "").startswith(INTENT_MESSAGE_PREFIX):
|
|
1195
|
+
return parse_data["text"]
|
|
1196
|
+
|
|
1197
|
+
if not parse_data.get("entities"):
|
|
1198
|
+
parse_data["entities"] = []
|
|
1199
|
+
|
|
1200
|
+
return TrainingDataWriter.generate_message(parse_data)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _validate_user_regex(latest_message: Dict[Text, Any], intents: List[Text]) -> bool:
|
|
1204
|
+
"""Validate if a users message input is correct.
|
|
1205
|
+
|
|
1206
|
+
This assumes the user entered an intent directly, e.g. using
|
|
1207
|
+
`/greet`. Return `True` if the intent is a known one.
|
|
1208
|
+
"""
|
|
1209
|
+
parse_data = latest_message.get("parse_data", {})
|
|
1210
|
+
intent = parse_data.get("intent", {}).get(INTENT_NAME_KEY)
|
|
1211
|
+
|
|
1212
|
+
if intent in intents:
|
|
1213
|
+
return True
|
|
1214
|
+
else:
|
|
1215
|
+
return False
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
async def _validate_user_text(
|
|
1219
|
+
latest_message: Dict[Text, Any], endpoint: EndpointConfig, conversation_id: Text
|
|
1220
|
+
) -> bool:
|
|
1221
|
+
"""Validate a user message input as free text.
|
|
1222
|
+
|
|
1223
|
+
This assumes the user message is a text message (so NOT `/greet`).
|
|
1224
|
+
"""
|
|
1225
|
+
parse_data = latest_message.get("parse_data", {})
|
|
1226
|
+
text = _as_md_message(parse_data)
|
|
1227
|
+
intent = parse_data.get("intent", {}).get(INTENT_NAME_KEY)
|
|
1228
|
+
entities = parse_data.get("entities", [])
|
|
1229
|
+
if entities:
|
|
1230
|
+
message = (
|
|
1231
|
+
f"Is the intent '{intent}' correct for '{text}' and are "
|
|
1232
|
+
f"all entities labeled correctly?"
|
|
1233
|
+
)
|
|
1234
|
+
else:
|
|
1235
|
+
message = (
|
|
1236
|
+
f"Your NLU model classified '{text}' with intent '{intent}'"
|
|
1237
|
+
f" and there are no entities, is this correct?"
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
if intent is None:
|
|
1241
|
+
print(f"The NLU classification for '{text}' returned '{intent}'")
|
|
1242
|
+
return False
|
|
1243
|
+
else:
|
|
1244
|
+
question = questionary.confirm(message)
|
|
1245
|
+
|
|
1246
|
+
return await _ask_questions(question, conversation_id, endpoint)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
async def _validate_nlu(
|
|
1250
|
+
intents: List[Text], endpoint: EndpointConfig, conversation_id: Text
|
|
1251
|
+
) -> None:
|
|
1252
|
+
"""Validate if a user message, either text or intent is correct.
|
|
1253
|
+
|
|
1254
|
+
If the prediction of the latest user message is incorrect,
|
|
1255
|
+
the tracker will be corrected with the correct intent / entities.
|
|
1256
|
+
"""
|
|
1257
|
+
tracker = await retrieve_tracker(
|
|
1258
|
+
endpoint, conversation_id, EventVerbosity.AFTER_RESTART
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
latest_message = latest_user_message(tracker.get("events", [])) or {}
|
|
1262
|
+
|
|
1263
|
+
if latest_message.get("text", "").startswith(INTENT_MESSAGE_PREFIX):
|
|
1264
|
+
valid = _validate_user_regex(latest_message, intents)
|
|
1265
|
+
else:
|
|
1266
|
+
valid = await _validate_user_text(latest_message, endpoint, conversation_id)
|
|
1267
|
+
|
|
1268
|
+
if not valid:
|
|
1269
|
+
corrected_intent = await _request_intent_from_user(
|
|
1270
|
+
latest_message, intents, conversation_id, endpoint
|
|
1271
|
+
)
|
|
1272
|
+
# corrected intents have confidence 1.0
|
|
1273
|
+
corrected_intent["confidence"] = 1.0
|
|
1274
|
+
|
|
1275
|
+
events = tracker.get("events", [])
|
|
1276
|
+
|
|
1277
|
+
entities = await _correct_entities(latest_message, endpoint, conversation_id)
|
|
1278
|
+
corrected_nlu = {
|
|
1279
|
+
"intent": corrected_intent,
|
|
1280
|
+
"entities": entities,
|
|
1281
|
+
"text": latest_message.get("text"),
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
await _correct_wrong_nlu(corrected_nlu, events, endpoint, conversation_id)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
async def _correct_entities(
|
|
1288
|
+
latest_message: Dict[Text, Any], endpoint: EndpointConfig, conversation_id: Text
|
|
1289
|
+
) -> List[Dict[Text, Any]]:
|
|
1290
|
+
"""Validate the entities of a user message.
|
|
1291
|
+
|
|
1292
|
+
Returns the corrected entities.
|
|
1293
|
+
"""
|
|
1294
|
+
from rasa.shared.nlu.training_data import entities_parser
|
|
1295
|
+
|
|
1296
|
+
parse_original = latest_message.get("parse_data", {})
|
|
1297
|
+
entity_str = _as_md_message(parse_original)
|
|
1298
|
+
question = questionary.text(
|
|
1299
|
+
"Please mark the entities using [value](type) notation", default=entity_str
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
annotation = await _ask_questions(question, conversation_id, endpoint)
|
|
1303
|
+
parse_annotated = entities_parser.parse_training_example(annotation)
|
|
1304
|
+
|
|
1305
|
+
corrected_entities = _merge_annotated_and_original_entities(
|
|
1306
|
+
parse_annotated, parse_original
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
return corrected_entities
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def _merge_annotated_and_original_entities(
|
|
1313
|
+
parse_annotated: Message, parse_original: Dict[Text, Any]
|
|
1314
|
+
) -> List[Dict[Text, Any]]:
|
|
1315
|
+
# overwrite entities which have already been
|
|
1316
|
+
# annotated in the original annotation to preserve
|
|
1317
|
+
# additional entity parser information
|
|
1318
|
+
entities = parse_annotated.get("entities", [])[:]
|
|
1319
|
+
for i, entity in enumerate(entities):
|
|
1320
|
+
for original_entity in parse_original.get("entities", []):
|
|
1321
|
+
if _is_same_entity_annotation(entity, original_entity):
|
|
1322
|
+
entities[i] = original_entity
|
|
1323
|
+
break
|
|
1324
|
+
return entities
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
def _is_same_entity_annotation(entity: Dict[Text, Any], other: Dict[Text, Any]) -> bool:
|
|
1328
|
+
return (
|
|
1329
|
+
entity["value"] == other["value"]
|
|
1330
|
+
and entity["entity"] == other["entity"]
|
|
1331
|
+
and entity.get("group") == other.get("group")
|
|
1332
|
+
and entity.get("role") == other.get("group")
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
async def _enter_user_message(conversation_id: Text, endpoint: EndpointConfig) -> None:
|
|
1337
|
+
"""Request a new message from the user."""
|
|
1338
|
+
question = questionary.text("Your input ->")
|
|
1339
|
+
|
|
1340
|
+
message = await _ask_questions(question, conversation_id, endpoint, lambda a: not a)
|
|
1341
|
+
|
|
1342
|
+
if message == (INTENT_MESSAGE_PREFIX + USER_INTENT_RESTART):
|
|
1343
|
+
raise RestartConversation()
|
|
1344
|
+
|
|
1345
|
+
await send_message(endpoint, conversation_id, message)
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
async def is_listening_for_message(
|
|
1349
|
+
conversation_id: Text, endpoint: EndpointConfig
|
|
1350
|
+
) -> bool:
|
|
1351
|
+
"""Check if the conversation is in need for a user message."""
|
|
1352
|
+
tracker = await retrieve_tracker(endpoint, conversation_id, EventVerbosity.APPLIED)
|
|
1353
|
+
|
|
1354
|
+
for i, e in enumerate(reversed(tracker.get("events", []))):
|
|
1355
|
+
if e.get("event") == UserUttered.type_name:
|
|
1356
|
+
return False
|
|
1357
|
+
elif e.get("event") == ActionExecuted.type_name:
|
|
1358
|
+
return e.get("name") == ACTION_LISTEN_NAME
|
|
1359
|
+
return False
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
async def _undo_latest(conversation_id: Text, endpoint: EndpointConfig) -> None:
|
|
1363
|
+
"""Undo either the latest bot action or user message, whatever is last."""
|
|
1364
|
+
tracker = await retrieve_tracker(endpoint, conversation_id, EventVerbosity.ALL)
|
|
1365
|
+
|
|
1366
|
+
# Get latest `UserUtterance` or `ActionExecuted` event.
|
|
1367
|
+
last_event_type = None
|
|
1368
|
+
for i, e in enumerate(reversed(tracker.get("events", []))):
|
|
1369
|
+
last_event_type = e.get("event")
|
|
1370
|
+
if last_event_type in {ActionExecuted.type_name, UserUttered.type_name}:
|
|
1371
|
+
break
|
|
1372
|
+
elif last_event_type == Restarted.type_name:
|
|
1373
|
+
break
|
|
1374
|
+
|
|
1375
|
+
if last_event_type == ActionExecuted.type_name:
|
|
1376
|
+
undo_action = ActionReverted().as_dict()
|
|
1377
|
+
await send_event(endpoint, conversation_id, undo_action)
|
|
1378
|
+
elif last_event_type == UserUttered.type_name:
|
|
1379
|
+
undo_user_message = UserUtteranceReverted().as_dict()
|
|
1380
|
+
listen_for_next_message = ActionExecuted(ACTION_LISTEN_NAME).as_dict()
|
|
1381
|
+
|
|
1382
|
+
await send_event(
|
|
1383
|
+
endpoint, conversation_id, [undo_user_message, listen_for_next_message]
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
async def _fetch_events(
|
|
1388
|
+
conversation_ids: List[Union[Text, List[Event]]], endpoint: EndpointConfig
|
|
1389
|
+
) -> List[List[Event]]:
|
|
1390
|
+
"""Retrieve all event trackers from the endpoint for all conversation ids."""
|
|
1391
|
+
event_sequences = []
|
|
1392
|
+
for conversation_id in conversation_ids:
|
|
1393
|
+
if isinstance(conversation_id, str):
|
|
1394
|
+
tracker = await retrieve_tracker(endpoint, conversation_id)
|
|
1395
|
+
events = tracker.get("events", [])
|
|
1396
|
+
|
|
1397
|
+
for conversation in _split_conversation_at_restarts(events):
|
|
1398
|
+
parsed_events = rasa.shared.core.events.deserialise_events(conversation)
|
|
1399
|
+
event_sequences.append(parsed_events)
|
|
1400
|
+
else:
|
|
1401
|
+
event_sequences.append(conversation_id)
|
|
1402
|
+
return event_sequences
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
async def _plot_trackers(
|
|
1406
|
+
conversation_ids: List[Union[Text, List[Event]]],
|
|
1407
|
+
output_file: Optional[Text],
|
|
1408
|
+
endpoint: EndpointConfig,
|
|
1409
|
+
unconfirmed: Optional[List[Event]] = None,
|
|
1410
|
+
) -> None:
|
|
1411
|
+
"""Create a plot of the trackers of the passed conversation ids.
|
|
1412
|
+
|
|
1413
|
+
This assumes that the last conversation id is the conversation we are currently
|
|
1414
|
+
working on. If there are events that are not part of this active tracker
|
|
1415
|
+
yet, they can be passed as part of `unconfirmed`. They will be appended
|
|
1416
|
+
to the currently active conversation.
|
|
1417
|
+
"""
|
|
1418
|
+
if not output_file or not conversation_ids:
|
|
1419
|
+
# if there is no output file provided, we are going to skip plotting
|
|
1420
|
+
# same happens if there are no conversation ids
|
|
1421
|
+
return
|
|
1422
|
+
|
|
1423
|
+
event_sequences = await _fetch_events(conversation_ids, endpoint)
|
|
1424
|
+
|
|
1425
|
+
if unconfirmed:
|
|
1426
|
+
event_sequences[-1].extend(unconfirmed)
|
|
1427
|
+
|
|
1428
|
+
graph = visualize_neighborhood(
|
|
1429
|
+
event_sequences[-1], event_sequences, output_file=None, max_history=2
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
from networkx.drawing.nx_pydot import write_dot
|
|
1433
|
+
|
|
1434
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
1435
|
+
write_dot(graph, f)
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def _print_help(skip_visualization: bool) -> None:
|
|
1439
|
+
"""Print some initial help message for the user."""
|
|
1440
|
+
if not skip_visualization:
|
|
1441
|
+
visualization_url = DEFAULT_SERVER_FORMAT.format(
|
|
1442
|
+
"http", DEFAULT_SERVER_PORT + 1
|
|
1443
|
+
)
|
|
1444
|
+
visualization_help = (
|
|
1445
|
+
f"Visualisation at {visualization_url}/visualization.html ."
|
|
1446
|
+
)
|
|
1447
|
+
else:
|
|
1448
|
+
visualization_help = ""
|
|
1449
|
+
|
|
1450
|
+
rasa.shared.utils.cli.print_success(
|
|
1451
|
+
f"Bot loaded. {visualization_help}\n"
|
|
1452
|
+
f"Type a message and press enter "
|
|
1453
|
+
f"(press 'Ctrl-c' to exit)."
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def intent_names_from_domain(domain: Any) -> List[Text]:
|
|
1458
|
+
"""Get a list of the possible intents names from the domain specification.
|
|
1459
|
+
|
|
1460
|
+
This is its own function as intents are non-trivial to unpack and this
|
|
1461
|
+
warrants testing.
|
|
1462
|
+
"""
|
|
1463
|
+
domain_intents = domain.get("intents", []) if domain is not None else []
|
|
1464
|
+
|
|
1465
|
+
# intents with properties such as `use_entities` or `ignore_entities`
|
|
1466
|
+
# are a dictionary which needs unpacking. Other intents are strings
|
|
1467
|
+
# and can be used as-is.
|
|
1468
|
+
return [next(iter(i)) if isinstance(i, dict) else i for i in domain_intents]
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
async def record_messages(
|
|
1472
|
+
endpoint: EndpointConfig,
|
|
1473
|
+
file_importer: TrainingDataImporter,
|
|
1474
|
+
conversation_id: Text = DEFAULT_SENDER_ID,
|
|
1475
|
+
max_message_limit: Optional[int] = None,
|
|
1476
|
+
skip_visualization: bool = False,
|
|
1477
|
+
) -> None:
|
|
1478
|
+
"""Read messages from the command line and print bot responses."""
|
|
1479
|
+
try:
|
|
1480
|
+
try:
|
|
1481
|
+
domain = await retrieve_domain(endpoint)
|
|
1482
|
+
except ClientError:
|
|
1483
|
+
logger.exception(
|
|
1484
|
+
f"Failed to connect to Rasa Core server at '{endpoint.url}'. "
|
|
1485
|
+
f"Is the server running?"
|
|
1486
|
+
)
|
|
1487
|
+
return
|
|
1488
|
+
|
|
1489
|
+
intents = intent_names_from_domain(domain)
|
|
1490
|
+
|
|
1491
|
+
num_messages = 0
|
|
1492
|
+
|
|
1493
|
+
if not skip_visualization:
|
|
1494
|
+
events_including_current_user_id = _get_tracker_events_to_plot(
|
|
1495
|
+
domain, file_importer, conversation_id
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
plot_file = DEFAULT_STORY_GRAPH_FILE
|
|
1499
|
+
await _plot_trackers(events_including_current_user_id, plot_file, endpoint)
|
|
1500
|
+
else:
|
|
1501
|
+
# `None` means that future `_plot_trackers` calls will also skip the
|
|
1502
|
+
# visualization.
|
|
1503
|
+
plot_file = None
|
|
1504
|
+
events_including_current_user_id = []
|
|
1505
|
+
|
|
1506
|
+
_print_help(skip_visualization)
|
|
1507
|
+
|
|
1508
|
+
while not utils.is_limit_reached(num_messages, max_message_limit):
|
|
1509
|
+
try:
|
|
1510
|
+
if await is_listening_for_message(conversation_id, endpoint):
|
|
1511
|
+
await _enter_user_message(conversation_id, endpoint)
|
|
1512
|
+
await _validate_nlu(intents, endpoint, conversation_id)
|
|
1513
|
+
|
|
1514
|
+
await _predict_till_next_listen(
|
|
1515
|
+
endpoint,
|
|
1516
|
+
conversation_id,
|
|
1517
|
+
events_including_current_user_id,
|
|
1518
|
+
plot_file,
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
num_messages += 1
|
|
1522
|
+
except RestartConversation:
|
|
1523
|
+
await send_event(endpoint, conversation_id, Restarted().as_dict())
|
|
1524
|
+
|
|
1525
|
+
await send_event(
|
|
1526
|
+
endpoint,
|
|
1527
|
+
conversation_id,
|
|
1528
|
+
ActionExecuted(ACTION_LISTEN_NAME).as_dict(),
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
logger.info("Restarted conversation, starting a new one.")
|
|
1532
|
+
except UndoLastStep:
|
|
1533
|
+
await _undo_latest(conversation_id, endpoint)
|
|
1534
|
+
await _print_history(conversation_id, endpoint)
|
|
1535
|
+
except ForkTracker:
|
|
1536
|
+
await _print_history(conversation_id, endpoint)
|
|
1537
|
+
|
|
1538
|
+
events_fork = await _request_fork_from_user(conversation_id, endpoint)
|
|
1539
|
+
|
|
1540
|
+
await send_event(endpoint, conversation_id, Restarted().as_dict())
|
|
1541
|
+
|
|
1542
|
+
if events_fork:
|
|
1543
|
+
for evt in events_fork:
|
|
1544
|
+
await send_event(endpoint, conversation_id, evt)
|
|
1545
|
+
logger.info("Restarted conversation at fork.")
|
|
1546
|
+
|
|
1547
|
+
await _print_history(conversation_id, endpoint)
|
|
1548
|
+
await _plot_trackers(
|
|
1549
|
+
events_including_current_user_id, plot_file, endpoint
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
except Abort:
|
|
1553
|
+
return
|
|
1554
|
+
except Exception:
|
|
1555
|
+
logger.exception("An exception occurred while recording messages.")
|
|
1556
|
+
raise
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _get_tracker_events_to_plot(
|
|
1560
|
+
domain: Dict[Text, Any], file_importer: TrainingDataImporter, conversation_id: Text
|
|
1561
|
+
) -> List[Union[Text, Deque[Event]]]:
|
|
1562
|
+
training_trackers = _get_training_trackers(file_importer, domain)
|
|
1563
|
+
number_of_trackers = len(training_trackers)
|
|
1564
|
+
if number_of_trackers > MAX_NUMBER_OF_TRAINING_STORIES_FOR_VISUALIZATION:
|
|
1565
|
+
rasa.shared.utils.cli.print_warning(
|
|
1566
|
+
f"You have {number_of_trackers} different story paths in "
|
|
1567
|
+
f"your training data. Visualizing them is very resource "
|
|
1568
|
+
f"consuming. Hence, the visualization will only show the stories "
|
|
1569
|
+
f"which you created during interactive learning, but not your "
|
|
1570
|
+
f"training stories."
|
|
1571
|
+
)
|
|
1572
|
+
training_trackers = []
|
|
1573
|
+
|
|
1574
|
+
training_data_events: List[Union[Text, Deque[Event]]] = [
|
|
1575
|
+
t.events for t in training_trackers
|
|
1576
|
+
]
|
|
1577
|
+
return training_data_events + [conversation_id]
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
def _get_training_trackers(
|
|
1581
|
+
file_importer: TrainingDataImporter, domain: Dict[str, Any]
|
|
1582
|
+
) -> List[TrackerWithCachedStates]:
|
|
1583
|
+
from rasa.core import training
|
|
1584
|
+
|
|
1585
|
+
return training.load_data(
|
|
1586
|
+
file_importer,
|
|
1587
|
+
Domain.from_dict(domain),
|
|
1588
|
+
augmentation_factor=0,
|
|
1589
|
+
use_story_concatenation=False,
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _serve_application(
|
|
1594
|
+
app: Sanic,
|
|
1595
|
+
file_importer: TrainingDataImporter,
|
|
1596
|
+
skip_visualization: bool,
|
|
1597
|
+
conversation_id: Text,
|
|
1598
|
+
port: int,
|
|
1599
|
+
) -> Sanic:
|
|
1600
|
+
"""Start a core server and attach the interactive learning IO."""
|
|
1601
|
+
endpoint = EndpointConfig(url=DEFAULT_SERVER_FORMAT.format("http", port))
|
|
1602
|
+
|
|
1603
|
+
@app.after_server_start
|
|
1604
|
+
async def run_interactive_io(running_app: Sanic) -> None:
|
|
1605
|
+
"""Small wrapper to shut down the server once cmd io is done."""
|
|
1606
|
+
await record_messages(
|
|
1607
|
+
endpoint=endpoint,
|
|
1608
|
+
file_importer=file_importer,
|
|
1609
|
+
skip_visualization=skip_visualization,
|
|
1610
|
+
conversation_id=conversation_id,
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
logger.info("Killing Sanic server now.")
|
|
1614
|
+
|
|
1615
|
+
running_app.stop() # kill the sanic server
|
|
1616
|
+
|
|
1617
|
+
update_sanic_log_level()
|
|
1618
|
+
|
|
1619
|
+
app.run(host="0.0.0.0", port=port, legacy=True)
|
|
1620
|
+
|
|
1621
|
+
return app
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def start_visualization(image_path: Text, port: int) -> None:
|
|
1625
|
+
"""Add routes to serve the conversation visualization files."""
|
|
1626
|
+
app = Sanic("rasa_interactive")
|
|
1627
|
+
|
|
1628
|
+
# Reset Sanic warnings filter that allows the triggering of Sanic warnings
|
|
1629
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module=r"sanic.*")
|
|
1630
|
+
|
|
1631
|
+
# noinspection PyUnusedLocal
|
|
1632
|
+
@app.exception(NotFound)
|
|
1633
|
+
async def ignore_404s(request: Request, exception: Exception) -> HTTPResponse:
|
|
1634
|
+
return response.text("Not found", status=404)
|
|
1635
|
+
|
|
1636
|
+
# noinspection PyUnusedLocal
|
|
1637
|
+
@app.route(VISUALIZATION_TEMPLATE_PATH, methods=["GET"])
|
|
1638
|
+
async def visualisation_html(request: Request) -> HTTPResponse:
|
|
1639
|
+
return await response.file(visualization.visualization_html_path())
|
|
1640
|
+
|
|
1641
|
+
# noinspection PyUnusedLocal
|
|
1642
|
+
@app.route("/visualization.dot", methods=["GET"])
|
|
1643
|
+
async def visualisation_png(request: Request) -> HTTPResponse:
|
|
1644
|
+
try:
|
|
1645
|
+
headers = {"Cache-Control": "no-cache"}
|
|
1646
|
+
return await response.file(os.path.abspath(image_path), headers=headers)
|
|
1647
|
+
except FileNotFoundError:
|
|
1648
|
+
return response.text("", 404)
|
|
1649
|
+
|
|
1650
|
+
update_sanic_log_level()
|
|
1651
|
+
|
|
1652
|
+
app.run(host="0.0.0.0", port=port, access_log=False, legacy=True)
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def run_interactive_learning(
|
|
1656
|
+
file_importer: TrainingDataImporter,
|
|
1657
|
+
skip_visualization: bool = False,
|
|
1658
|
+
conversation_id: Text = uuid.uuid4().hex,
|
|
1659
|
+
server_args: Optional[Dict[Text, Any]] = None,
|
|
1660
|
+
) -> None:
|
|
1661
|
+
"""Start the interactive learning with the model of the agent."""
|
|
1662
|
+
global SAVE_IN_E2E
|
|
1663
|
+
server_args = server_args or {}
|
|
1664
|
+
|
|
1665
|
+
if server_args.get("nlu_data"):
|
|
1666
|
+
PATHS["nlu"] = server_args["nlu_data"]
|
|
1667
|
+
|
|
1668
|
+
if server_args.get("stories"):
|
|
1669
|
+
PATHS["stories"] = server_args["stories"]
|
|
1670
|
+
|
|
1671
|
+
if server_args.get("domain"):
|
|
1672
|
+
PATHS["domain"] = server_args["domain"]
|
|
1673
|
+
|
|
1674
|
+
port = server_args.get("port", DEFAULT_SERVER_PORT)
|
|
1675
|
+
|
|
1676
|
+
SAVE_IN_E2E = server_args["e2e"]
|
|
1677
|
+
|
|
1678
|
+
if not skip_visualization:
|
|
1679
|
+
visualisation_port = port + 1
|
|
1680
|
+
p = Process(
|
|
1681
|
+
target=start_visualization,
|
|
1682
|
+
args=(DEFAULT_STORY_GRAPH_FILE, visualisation_port),
|
|
1683
|
+
daemon=True,
|
|
1684
|
+
)
|
|
1685
|
+
p.start()
|
|
1686
|
+
else:
|
|
1687
|
+
p = None
|
|
1688
|
+
|
|
1689
|
+
app = run.configure_app(port=port, conversation_id="default", enable_api=True)
|
|
1690
|
+
endpoints = AvailableEndpoints.get_instance(server_args.get("endpoints"))
|
|
1691
|
+
|
|
1692
|
+
# before_server_start handlers make sure the agent is loaded before the
|
|
1693
|
+
# interactive learning IO starts
|
|
1694
|
+
app.register_listener(
|
|
1695
|
+
partial(run.load_agent_on_start, server_args.get("model"), endpoints, None),
|
|
1696
|
+
"before_server_start",
|
|
1697
|
+
)
|
|
1698
|
+
|
|
1699
|
+
telemetry.track_interactive_learning_start(skip_visualization, SAVE_IN_E2E)
|
|
1700
|
+
|
|
1701
|
+
_serve_application(app, file_importer, skip_visualization, conversation_id, port)
|
|
1702
|
+
|
|
1703
|
+
if not skip_visualization and p is not None:
|
|
1704
|
+
p.terminate()
|
|
1705
|
+
p.join()
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
def calc_true_wrapping_width(text: Text, monospace_wrapping_width: int) -> int:
|
|
1709
|
+
"""Calculates a wrapping width that also works for CJK characters.
|
|
1710
|
+
|
|
1711
|
+
Chinese, Japanese and Korean characters are often broader than ascii
|
|
1712
|
+
characters:
|
|
1713
|
+
abcdefgh (8 chars)
|
|
1714
|
+
我è¦åŽ»åŒ—äº¬ (5 chars, roughly same visible width)
|
|
1715
|
+
|
|
1716
|
+
We need to account for that otherwise the wrapping doesn't work
|
|
1717
|
+
appropriately for long strings and the table overflows and creates
|
|
1718
|
+
errors.
|
|
1719
|
+
|
|
1720
|
+
params:
|
|
1721
|
+
text: text sequence that should be wrapped into multiple lines
|
|
1722
|
+
monospace_wrapping_width: the maximum width per line in number of
|
|
1723
|
+
standard ascii characters
|
|
1724
|
+
returns:
|
|
1725
|
+
The maximum line width for the given string that takes into account
|
|
1726
|
+
the strings visible width, so that it won't lead to table overflow.
|
|
1727
|
+
"""
|
|
1728
|
+
true_wrapping_width = 0
|
|
1729
|
+
|
|
1730
|
+
# testing potential width from longest to shortest
|
|
1731
|
+
for potential_width in range(monospace_wrapping_width, -1, -1):
|
|
1732
|
+
lines = textwrap.wrap(text, potential_width)
|
|
1733
|
+
# test whether all lines' visible width fits the available width
|
|
1734
|
+
if all(
|
|
1735
|
+
[
|
|
1736
|
+
terminaltables.width_and_alignment.visible_width(line)
|
|
1737
|
+
<= monospace_wrapping_width
|
|
1738
|
+
for line in lines
|
|
1739
|
+
]
|
|
1740
|
+
):
|
|
1741
|
+
true_wrapping_width = potential_width
|
|
1742
|
+
break
|
|
1743
|
+
|
|
1744
|
+
return true_wrapping_width
|