rasa-pro 3.13.1a18__py3-none-any.whl → 3.13.1a20__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.
- rasa/__main__.py +8 -0
- rasa/builder/auth.py +71 -0
- rasa/builder/config.py +16 -0
- rasa/builder/copilot/constants.py +15 -0
- rasa/builder/copilot/copilot.py +342 -0
- rasa/builder/copilot/copilot_response_handler.py +471 -0
- rasa/builder/copilot/exceptions.py +20 -0
- rasa/builder/copilot/models.py +344 -0
- rasa/builder/copilot/prompts/copilot_system_prompt.jinja2 +495 -0
- rasa/builder/copilot/telemetry.py +195 -0
- rasa/builder/document_retrieval/__init__.py +0 -0
- rasa/builder/document_retrieval/constants.py +16 -0
- rasa/builder/{inkeep_document_retrieval.py → document_retrieval/inkeep_document_retrieval.py} +53 -44
- rasa/builder/document_retrieval/models.py +62 -0
- rasa/builder/download.py +140 -0
- rasa/builder/guardrails/__init__.py +1 -0
- rasa/builder/guardrails/constants.py +4 -0
- rasa/builder/guardrails/exceptions.py +4 -0
- rasa/builder/guardrails/lakera.py +188 -0
- rasa/builder/guardrails/models.py +199 -0
- rasa/builder/guardrails/utils.py +305 -0
- rasa/builder/job_manager.py +87 -0
- rasa/builder/jobs.py +232 -0
- rasa/builder/llm_service.py +89 -173
- rasa/builder/logging_utils.py +162 -4
- rasa/builder/main.py +29 -16
- rasa/builder/models.py +93 -121
- rasa/builder/project_generator.py +91 -7
- rasa/builder/scrape_rasa_docs.py +1 -1
- rasa/builder/service.py +650 -452
- rasa/builder/shared/tracker_context.py +212 -0
- rasa/builder/validation_service.py +4 -4
- rasa/cli/data.py +8 -3
- rasa/cli/project_templates/basic/actions/action_api.py +15 -0
- rasa/cli/project_templates/basic/actions/action_human_handoff.py +44 -0
- rasa/cli/project_templates/basic/config.yml +23 -0
- rasa/cli/project_templates/{plain → basic}/credentials.yml +8 -7
- rasa/cli/project_templates/basic/data/general/feedback.yml +20 -0
- rasa/cli/project_templates/basic/data/general/goodbye.yml +6 -0
- rasa/cli/project_templates/basic/data/general/hello.yml +7 -0
- rasa/cli/project_templates/basic/data/general/help.yml +6 -0
- rasa/cli/project_templates/basic/data/general/human_handoff.yml +16 -0
- rasa/cli/project_templates/basic/data/general/welcome.yml +9 -0
- rasa/cli/project_templates/{finance/data/patterns → basic/data/system}/pattern_completed.yml +2 -1
- rasa/cli/project_templates/basic/data/system/pattern_correction.yml +7 -0
- rasa/cli/project_templates/basic/data/system/pattern_search.yml +8 -0
- rasa/cli/project_templates/basic/data/system/pattern_session_start.yml +8 -0
- rasa/cli/project_templates/basic/docs/rasa_assistant_qa.txt +65 -0
- rasa/cli/project_templates/basic/docs/template.txt +7 -0
- rasa/cli/project_templates/basic/domain/general/assistant_details.yml +12 -0
- rasa/cli/project_templates/basic/domain/general/bot_identity.yml +5 -0
- rasa/cli/project_templates/basic/domain/general/cannot_handle.yml +5 -0
- rasa/cli/project_templates/basic/domain/general/feedback.yml +28 -0
- rasa/cli/project_templates/basic/domain/general/goodbye.yml +7 -0
- rasa/cli/project_templates/basic/domain/general/help.yml +5 -0
- rasa/cli/project_templates/basic/domain/general/human_handoff_domain.yml +35 -0
- rasa/cli/project_templates/{finance/domain/default_actions.yml → basic/domain/general/utils.yml} +0 -3
- rasa/cli/project_templates/basic/domain/general/welcome.yml +7 -0
- rasa/cli/project_templates/{plain → basic}/endpoints.yml +42 -27
- rasa/cli/project_templates/basic/prompts/rephraser_demo_personality_prompt.jinja2 +19 -0
- rasa/cli/project_templates/defaults.py +25 -3
- rasa/cli/project_templates/finance/actions/__init__.py +46 -0
- rasa/cli/project_templates/finance/actions/accounts/__init__.py +0 -0
- rasa/cli/project_templates/finance/actions/{action_ask_account.py → accounts/action_ask_account.py} +6 -9
- rasa/cli/project_templates/finance/actions/{action_check_balance.py → accounts/action_check_balance.py} +4 -4
- rasa/cli/project_templates/finance/actions/action_session_start.py +11 -6
- rasa/cli/project_templates/finance/actions/cards/__init__.py +0 -0
- rasa/cli/project_templates/finance/actions/{action_ask_card.py → cards/action_ask_card.py} +4 -3
- rasa/cli/project_templates/finance/actions/{action_check_card_existence.py → cards/action_check_card_existence.py} +4 -3
- rasa/cli/project_templates/finance/actions/{action_update_card_status.py → cards/action_update_card_status.py} +18 -9
- rasa/cli/project_templates/finance/actions/database.py +1 -0
- rasa/cli/project_templates/finance/actions/transfers/__init__.py +0 -0
- rasa/cli/project_templates/finance/actions/{action_add_payee.py → transfers/action_add_payee.py} +8 -3
- rasa/cli/project_templates/finance/actions/{action_ask_account_from.py → transfers/action_ask_account_from.py} +5 -4
- rasa/cli/project_templates/finance/actions/{action_check_payee_existence.py → transfers/action_check_payee_existence.py} +3 -3
- rasa/cli/project_templates/finance/actions/{action_check_sufficient_funds.py → transfers/action_check_sufficient_funds.py} +3 -4
- rasa/cli/project_templates/finance/actions/{action_list_payees.py → transfers/action_list_payees.py} +4 -3
- rasa/cli/project_templates/finance/actions/{action_remove_payee.py → transfers/action_remove_payee.py} +4 -4
- rasa/cli/project_templates/finance/config.yml +8 -19
- rasa/cli/project_templates/finance/credentials.yml +6 -7
- rasa/cli/project_templates/finance/csvs/cards.csv +10 -10
- rasa/cli/project_templates/finance/csvs/payees.csv +10 -9
- rasa/cli/project_templates/finance/data/{flows → accounts}/check_balance.yml +2 -1
- rasa/cli/project_templates/finance/data/general/bot_identity.yml +6 -0
- rasa/cli/project_templates/finance/data/general/feedback.yml +20 -0
- rasa/cli/project_templates/finance/data/general/goodbye.yml +6 -0
- rasa/cli/project_templates/finance/data/general/hello.yml +7 -0
- rasa/cli/project_templates/finance/data/{flows/welcome.yml → general/help.yml} +2 -7
- rasa/cli/project_templates/finance/data/general/human_handoff.yml +16 -0
- rasa/cli/project_templates/finance/data/general/welcome.yml +9 -0
- rasa/cli/project_templates/finance/data/{patterns → system/patterns}/pattern_chitchat.yml +0 -2
- rasa/cli/project_templates/finance/data/system/patterns/pattern_completed.yml +7 -0
- rasa/cli/project_templates/finance/data/system/patterns/pattern_correction.yml +7 -0
- rasa/cli/project_templates/finance/data/system/patterns/pattern_search.yml +8 -0
- rasa/cli/project_templates/finance/data/{patterns → system/patterns}/pattern_session_start.yml +0 -1
- rasa/cli/project_templates/finance/domain/{check_balance.yml → accounts/check_balance.yml} +2 -0
- rasa/cli/project_templates/finance/domain/general/assistant_details.yml +12 -0
- rasa/cli/project_templates/finance/domain/general/bot_identity.yml +5 -0
- rasa/cli/project_templates/finance/domain/general/cannot_handle.yml +5 -0
- rasa/cli/project_templates/finance/domain/general/defaults.yml +24 -0
- rasa/cli/project_templates/finance/domain/general/feedback.yml +28 -0
- rasa/cli/project_templates/finance/domain/general/goodbye.yml +7 -0
- rasa/cli/project_templates/finance/domain/general/help.yml +5 -0
- rasa/cli/project_templates/finance/domain/general/human_handoff.yml +30 -0
- rasa/cli/project_templates/finance/domain/general/utils.yml +13 -0
- rasa/cli/project_templates/finance/domain/general/welcome.yml +8 -0
- rasa/cli/project_templates/finance/endpoints.yml +1 -0
- rasa/cli/project_templates/finance/prompts/rephraser_demo_personality_prompt.jinja2 +3 -3
- rasa/cli/project_templates/telco/actions/actions_billing.py +24 -17
- rasa/cli/project_templates/telco/actions/actions_get_data_from_db.py +6 -1
- rasa/cli/project_templates/telco/actions/actions_run_diagnostics.py +6 -1
- rasa/cli/project_templates/telco/actions/actions_session_start.py +6 -1
- rasa/cli/project_templates/tutorial/config.yml +2 -1
- rasa/cli/scaffold.py +27 -2
- rasa/cli/train.py +8 -0
- rasa/cli/utils.py +31 -15
- rasa/core/actions/action.py +28 -41
- rasa/core/actions/action_run_slot_rejections.py +1 -1
- rasa/core/channels/development_inspector.py +47 -14
- rasa/core/channels/inspector/dist/assets/{arc-371401b1.js → arc-1ddec37b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-3f126156.js → blockDiagram-38ab4fdb-18af387c.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-12f22eb7.js → c4Diagram-3d4e48cf-250127a3.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-59f6d54b.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-03b1d386.js → classDiagram-70f12bd4-c3388b34.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-84f69d63.js → classDiagram-v2-f2320105-9c893a82.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-26177ddb.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-ca47fd38.js → createText-2e5e7dd3-c111213b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-f837ca8a.js → edges-e0da2a9e-812a729d.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-8717ac54.js → erDiagram-9861fffd-fd5051bc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-94f38b83.js → flowDb-956e92f1-3287ac02.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-b616f9fb.js → flowDiagram-66a62f08-692fb0b2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-29c03f5a.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-f5d24bb8.js → flowchart-elk-definition-4a651766-008376f1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-b43ba8d9.js → ganttDiagram-c361ad54-df330a69.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-c3aafaa5.js → gitGraphDiagram-72cf32ee-e03676fb.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-0d0a2c10.js → graph-46fad2ba.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-3862675e-58ea0305.js → index-3862675e-a484ac55.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-cce6f8a1.js → index-a003633f.js} +179 -179
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-b8f60461.js → infoDiagram-f8f76790-3f9e6ec2.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-95be5545.js → journeyDiagram-49397b02-79f72383.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-da885b9b.js → layout-aad098e5.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-f1c817d3.js → line-219ab7ae.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-d42801e6.js → linear-2cddbe62.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-a38923a6.js → mindmap-definition-fc14e90a-1d41ed99.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-ca6e71e9.js → pieDiagram-8a3498a8-cc496ee8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-b290dae9.js → quadrantDiagram-120e2f19-84d32884.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-03f02ceb.js → requirementDiagram-deff3bca-c0deb984.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-c49eee40.js → sankeyDiagram-04a897e0-b9d7fd62.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-b2cd6a3d.js → sequenceDiagram-704730f1-7d517565.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-e53a2028.js → stateDiagram-587899a1-98ef9b27.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-e1982a03.js → stateDiagram-v2-d93cdb3a-cee70748.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-d0226ca5.js → styles-6aaf32cf-3f9d1c96.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-0e21dc00.js → styles-9a916d00-67471923.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-9588494e.js → styles-c10674c1-bd093fb7.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-be478d4f.js → svgDrawCommon-08f97a94-675794e8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-74631749.js → timeline-definition-85554ec2-0ac67617.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-a043552f.js → xychartDiagram-e933f94c-c018dc37.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +2 -2
- rasa/core/channels/inspector/index.html +1 -1
- rasa/core/channels/inspector/package.json +4 -3
- rasa/core/channels/inspector/src/App.tsx +53 -7
- rasa/core/channels/inspector/src/components/Chat.tsx +3 -2
- rasa/core/channels/inspector/src/components/DiagramFlow.tsx +1 -1
- rasa/core/channels/inspector/src/components/LatencyDisplay.tsx +268 -0
- rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -2
- rasa/core/channels/inspector/src/helpers/audio/audiostream.ts +8 -3
- rasa/core/channels/inspector/src/types.ts +8 -0
- rasa/core/channels/inspector/yarn.lock +12 -12
- rasa/core/channels/studio_chat.py +119 -34
- rasa/core/channels/voice_ready/twilio_voice.py +1 -1
- rasa/core/channels/voice_stream/asr/asr_engine.py +5 -1
- rasa/core/channels/voice_stream/asr/deepgram.py +5 -0
- rasa/core/channels/voice_stream/audiocodes.py +16 -8
- rasa/core/channels/voice_stream/browser_audio.py +39 -4
- rasa/core/channels/voice_stream/call_state.py +13 -2
- rasa/core/channels/voice_stream/genesys.py +16 -13
- rasa/core/channels/voice_stream/jambonz.py +14 -12
- rasa/core/channels/voice_stream/twilio_media_streams.py +14 -13
- rasa/core/channels/voice_stream/util.py +11 -1
- rasa/core/channels/voice_stream/voice_channel.py +108 -29
- rasa/core/nlg/callback.py +1 -1
- rasa/core/nlg/contextual_response_rephraser.py +19 -9
- rasa/core/nlg/generator.py +21 -5
- rasa/core/nlg/response.py +43 -6
- rasa/core/nlg/translate.py +8 -0
- rasa/core/policies/enterprise_search_policy.py +16 -21
- rasa/dialogue_understanding/commands/correct_slots_command.py +38 -10
- rasa/dialogue_understanding/generator/command_generator.py +5 -5
- rasa/dialogue_understanding/generator/command_parser.py +9 -13
- rasa/dialogue_understanding/processor/command_processor.py +149 -55
- rasa/dialogue_understanding/stack/utils.py +13 -3
- rasa/dialogue_understanding_test/du_test_schema.yml +3 -3
- rasa/dialogue_understanding_test/validation.py +9 -10
- rasa/e2e_test/e2e_config.py +18 -11
- rasa/e2e_test/e2e_test_schema.yml +3 -3
- rasa/e2e_test/utils/validation.py +17 -19
- rasa/engine/validation.py +86 -91
- rasa/exceptions.py +26 -1
- rasa/model_manager/model_api.py +2 -2
- rasa/model_manager/socket_bridge.py +8 -2
- rasa/shared/providers/_configs/default_litellm_client_config.py +3 -7
- rasa/shared/utils/cli.py +2 -0
- rasa/shared/utils/common.py +2 -1
- rasa/shared/utils/health_check/health_check.py +10 -14
- rasa/studio/upload.py +6 -2
- rasa/studio/utils.py +33 -22
- rasa/telemetry.py +95 -22
- rasa/utils/licensing.py +21 -10
- rasa/utils/log_utils.py +1 -1
- rasa/utils/tensorflow/transformer.py +3 -3
- rasa/validator.py +7 -5
- rasa/version.py +1 -1
- {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/METADATA +7 -7
- {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/RECORD +242 -203
- rasa/builder/create_openai_vector_store.py +0 -228
- rasa/builder/llm-helper-schema.json +0 -69
- rasa/builder/llm_context.py +0 -81
- rasa/builder/llm_helper_prompt.jinja2 +0 -245
- rasa/cli/project_templates/finance/data/nlu.yml +0 -29
- rasa/cli/project_templates/finance/data/patterns/pattern_search.yml +0 -5
- rasa/cli/project_templates/finance/domain/default_flows.yml +0 -33
- rasa/cli/project_templates/finance/prompts/command-generator.jinja2 +0 -57
- rasa/cli/project_templates/finance/tests/conversation_repair/cancellations.yml +0 -12
- rasa/cli/project_templates/finance/tests/conversation_repair/cannot_handle.yml +0 -7
- rasa/cli/project_templates/finance/tests/conversation_repair/chitchat.yml +0 -7
- rasa/cli/project_templates/finance/tests/conversation_repair/clarification.yml +0 -9
- rasa/cli/project_templates/finance/tests/conversation_repair/completion.yml +0 -18
- rasa/cli/project_templates/finance/tests/conversation_repair/corrections.yml +0 -17
- rasa/cli/project_templates/finance/tests/conversation_repair/digressions.yml +0 -32
- rasa/cli/project_templates/finance/tests/conversation_repair/human_handoff.yml +0 -21
- rasa/cli/project_templates/finance/tests/conversation_repair/skipping_collect_steps.yml +0 -16
- rasa/cli/project_templates/finance/tests/demo_scripts/main.yml +0 -16
- rasa/cli/project_templates/finance/tests/happy_paths/balance_verification.yml +0 -15
- rasa/cli/project_templates/finance/tests/happy_paths/banking_questions.yml +0 -12
- rasa/cli/project_templates/finance/tests/happy_paths/card_blocking.yml +0 -52
- rasa/cli/project_templates/finance/tests/happy_paths/money_transfer.yml +0 -136
- rasa/cli/project_templates/finance/tests/happy_paths/payee_management.yml +0 -27
- rasa/cli/project_templates/finance/tests/happy_paths/user_greeted.yml +0 -5
- rasa/cli/project_templates/plain/config.yml +0 -17
- rasa/cli/project_templates/plain/data/patterns/pattern_session_start.yml +0 -7
- rasa/cli/project_templates/plain/domain.yml +0 -5
- rasa/core/channels/inspector/dist/assets/channel-f1efda17.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-fdf164e2.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-7d7a1629.js +0 -1
- rasa/shared/importers/static.py +0 -63
- /rasa/{cli/project_templates/plain/actions → builder/copilot}/__init__.py +0 -0
- /rasa/builder/{inkeep-rag-response-schema.json → document_retrieval/inkeep-rag-response-schema.json} +0 -0
- /rasa/cli/project_templates/finance/actions/{action_process_immediate_payment.py → transfers/action_process_immediate_payment.py} +0 -0
- /rasa/cli/project_templates/finance/actions/{action_schedule_payment.py → transfers/action_schedule_payment.py} +0 -0
- /rasa/cli/project_templates/finance/actions/{action_validate_payment_date.py → transfers/action_validate_payment_date.py} +0 -0
- /rasa/cli/project_templates/finance/data/{flows → cards}/block_card.yml +0 -0
- /rasa/cli/project_templates/finance/data/{flows → cards}/select_card.yml +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/accounts.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/advisors.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/appointments.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/branches.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/cards.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/payees.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/transactions.json +0 -0
- /rasa/cli/project_templates/finance/data/{source → system/source}/users.json +0 -0
- /rasa/cli/project_templates/finance/data/{flows → transfers}/add_payee.yml +0 -0
- /rasa/cli/project_templates/finance/data/{flows → transfers}/list_payees.yml +0 -0
- /rasa/cli/project_templates/finance/data/{flows → transfers}/remove_payee.yml +0 -0
- /rasa/cli/project_templates/finance/data/{flows → transfers}/transfer_money.yml +0 -0
- /rasa/cli/project_templates/finance/domain/{block_card.yml → cards/block_card.yml} +0 -0
- /rasa/cli/project_templates/finance/domain/{select_card.yml → cards/select_card.yml} +0 -0
- /rasa/cli/project_templates/finance/domain/{add_payee.yml → transfers/add_payee.yml} +0 -0
- /rasa/cli/project_templates/finance/domain/{list_payees.yml → transfers/list_payees.yml} +0 -0
- /rasa/cli/project_templates/finance/domain/{remove_payee.yml → transfers/remove_payee.yml} +0 -0
- /rasa/cli/project_templates/finance/domain/{transfer_money.yml → transfers/transfer_money.yml} +0 -0
- {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/entry_points.txt +0 -0
rasa/builder/service.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# mypy: disable-error-code=misc
|
|
2
|
+
import asyncio
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from http import HTTPStatus
|
|
4
7
|
from typing import Any, Optional
|
|
5
8
|
|
|
6
9
|
import structlog
|
|
@@ -8,30 +11,61 @@ from sanic import Blueprint, HTTPResponse, response
|
|
|
8
11
|
from sanic.request import Request
|
|
9
12
|
from sanic_openapi import openapi
|
|
10
13
|
|
|
11
|
-
from rasa.builder.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
from rasa.builder.auth import HEADER_USER_ID, extract_and_verify_auth0_token
|
|
15
|
+
from rasa.builder.config import (
|
|
16
|
+
COPILOT_ASSISTANT_TRACKER_MAX_TURNS,
|
|
17
|
+
COPILOT_HANDLER_ROLLING_BUFFER_SIZE,
|
|
18
|
+
HELLO_RASA_PROJECT_ID,
|
|
19
|
+
)
|
|
20
|
+
from rasa.builder.copilot.constants import ROLE_USER
|
|
21
|
+
from rasa.builder.copilot.exceptions import CopilotStreamError
|
|
22
|
+
from rasa.builder.copilot.models import (
|
|
23
|
+
CopilotContext,
|
|
24
|
+
CopilotRequest,
|
|
25
|
+
GeneratedContent,
|
|
26
|
+
ReferenceEntry,
|
|
27
|
+
ReferenceSection,
|
|
28
|
+
ResponseCategory,
|
|
29
|
+
ResponseCompleteness,
|
|
30
|
+
)
|
|
31
|
+
from rasa.builder.copilot.telemetry import CopilotTelemetry
|
|
32
|
+
from rasa.builder.download import create_bot_project_archive
|
|
33
|
+
from rasa.builder.guardrails.utils import (
|
|
34
|
+
check_assistant_chat_for_policy_violations,
|
|
35
|
+
check_copilot_chat_for_policy_violations,
|
|
36
|
+
)
|
|
37
|
+
from rasa.builder.job_manager import job_manager
|
|
38
|
+
from rasa.builder.jobs import (
|
|
39
|
+
run_prompt_to_bot_job,
|
|
40
|
+
run_template_to_bot_job,
|
|
41
|
+
run_update_files_job,
|
|
16
42
|
)
|
|
17
43
|
from rasa.builder.llm_service import llm_service
|
|
18
|
-
from rasa.builder.logging_utils import
|
|
44
|
+
from rasa.builder.logging_utils import (
|
|
45
|
+
capture_exception_with_context,
|
|
46
|
+
get_recent_logs,
|
|
47
|
+
)
|
|
19
48
|
from rasa.builder.models import (
|
|
20
49
|
ApiErrorResponse,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
50
|
+
AssistantInfo,
|
|
51
|
+
BotData,
|
|
52
|
+
JobCreateResponse,
|
|
53
|
+
JobStatus,
|
|
54
|
+
JobStatusEvent,
|
|
24
55
|
PromptRequest,
|
|
25
56
|
ServerSentEvent,
|
|
26
57
|
TemplateRequest,
|
|
27
58
|
)
|
|
28
59
|
from rasa.builder.project_generator import ProjectGenerator
|
|
29
|
-
from rasa.builder.
|
|
30
|
-
from rasa.
|
|
31
|
-
from rasa.cli.scaffold import ProjectTemplateName
|
|
60
|
+
from rasa.builder.shared.tracker_context import TrackerContext
|
|
61
|
+
from rasa.core.agent import Agent
|
|
32
62
|
from rasa.core.channels.studio_chat import StudioChatInput
|
|
63
|
+
from rasa.core.exceptions import AgentNotReady
|
|
64
|
+
from rasa.shared.core.flows.flows_list import FlowsList
|
|
65
|
+
from rasa.shared.core.flows.yaml_flows_io import get_flows_as_json
|
|
33
66
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
34
|
-
from rasa.
|
|
67
|
+
from rasa.shared.importers.utils import DOMAIN_KEYS
|
|
68
|
+
from rasa.utils.json_utils import extract_values
|
|
35
69
|
from rasa.utils.openapi import model_to_schema
|
|
36
70
|
|
|
37
71
|
structlogger = structlog.get_logger()
|
|
@@ -51,6 +85,10 @@ def setup_project_generator(project_folder: Optional[str] = None) -> ProjectGene
|
|
|
51
85
|
# for relative paths (./docs) in a projects config to work
|
|
52
86
|
os.chdir(project_folder)
|
|
53
87
|
|
|
88
|
+
# Ensure the project folder is in sys.path
|
|
89
|
+
if project_folder not in sys.path:
|
|
90
|
+
sys.path.insert(0, project_folder)
|
|
91
|
+
|
|
54
92
|
structlogger.info(
|
|
55
93
|
"bot_builder_service.service_initialized", project_folder=project_folder
|
|
56
94
|
)
|
|
@@ -68,34 +106,24 @@ def get_input_channel(request: Request) -> StudioChatInput:
|
|
|
68
106
|
return request.app.ctx.input_channel
|
|
69
107
|
|
|
70
108
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
) -> CALMUserData:
|
|
74
|
-
"""Extract CALMUserData from a ProjectGenerator.
|
|
109
|
+
async def extract_bot_data_from_agent(agent: Agent) -> BotData:
|
|
110
|
+
"""Extract BotData from an Agent.
|
|
75
111
|
|
|
76
112
|
Args:
|
|
77
|
-
|
|
113
|
+
agent: The agent to extract data from
|
|
78
114
|
|
|
79
115
|
Returns:
|
|
80
|
-
|
|
116
|
+
BotData containing flows, domain, config, endpoints, and nlu data
|
|
81
117
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else:
|
|
92
|
-
endpoints = {}
|
|
93
|
-
|
|
94
|
-
# Use the shared function with the importer and project data paths
|
|
95
|
-
return extract_calm_import_parts_from_importer(
|
|
96
|
-
importer=importer,
|
|
97
|
-
config=None, # Let the shared function get config from importer
|
|
98
|
-
endpoints=endpoints,
|
|
118
|
+
domain = agent.domain.as_dict() if agent.domain else {}
|
|
119
|
+
flows = (
|
|
120
|
+
await agent.processor.get_flows()
|
|
121
|
+
if agent.processor
|
|
122
|
+
else FlowsList(underlying_flows=[])
|
|
123
|
+
)
|
|
124
|
+
return BotData(
|
|
125
|
+
flows=get_flows_as_json(flows),
|
|
126
|
+
domain=extract_values(domain, DOMAIN_KEYS),
|
|
99
127
|
)
|
|
100
128
|
|
|
101
129
|
|
|
@@ -110,38 +138,122 @@ async def health(request: Request) -> HTTPResponse:
|
|
|
110
138
|
return response.json({"status": "ok", "service": "bot-builder"})
|
|
111
139
|
|
|
112
140
|
|
|
141
|
+
@bp.route("/job-events/<job_id>", methods=["GET"])
|
|
142
|
+
@openapi.summary("Stream job progress events")
|
|
143
|
+
@openapi.description(
|
|
144
|
+
"Stream server-sent events (SSE) tracking real-time job progress.\n\n"
|
|
145
|
+
"**Connect with:** `Accept: text/event-stream`.\n\n"
|
|
146
|
+
"**SSE Event Example:**\n"
|
|
147
|
+
"```text\n"
|
|
148
|
+
"event: received\n"
|
|
149
|
+
'data: {"status": "received"}\n'
|
|
150
|
+
"```\n\n"
|
|
151
|
+
)
|
|
152
|
+
@openapi.tag("job-events")
|
|
153
|
+
@openapi.parameter(
|
|
154
|
+
"job_id", str, location="path", description="The id of the job to stream events for"
|
|
155
|
+
)
|
|
156
|
+
@openapi.response(
|
|
157
|
+
200,
|
|
158
|
+
{"text/event-stream": str},
|
|
159
|
+
description="Server-sent events stream. See documentation for event types.",
|
|
160
|
+
example=(
|
|
161
|
+
"event: received\n"
|
|
162
|
+
'data: {"status": "received"}\n'
|
|
163
|
+
"\n"
|
|
164
|
+
"event: generating\n"
|
|
165
|
+
'data: {"status": "generating"}\n'
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
@openapi.response(
|
|
169
|
+
404,
|
|
170
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
171
|
+
description="Unknown job_id: No such job exists.",
|
|
172
|
+
)
|
|
173
|
+
async def job_events(request: Request, job_id: str) -> HTTPResponse:
|
|
174
|
+
try:
|
|
175
|
+
job = job_manager.get_job(job_id)
|
|
176
|
+
if job is None:
|
|
177
|
+
return response.json(
|
|
178
|
+
ApiErrorResponse(
|
|
179
|
+
error="Job not found", details={"job_id": job_id}
|
|
180
|
+
).model_dump(),
|
|
181
|
+
status=404,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
stream = await request.respond(content_type="text/event-stream")
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
async for evt in job.event_stream():
|
|
188
|
+
await stream.send(evt.format())
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
# Handle exceptions within the SSE stream context
|
|
191
|
+
capture_exception_with_context(
|
|
192
|
+
exc,
|
|
193
|
+
"bot_builder_service.job_events.streaming_error",
|
|
194
|
+
extra={"job_id": job_id},
|
|
195
|
+
tags={"endpoint": "/api/job-events/<job_id>"},
|
|
196
|
+
)
|
|
197
|
+
# Send error event in SSE format instead of JSON response
|
|
198
|
+
error_event = JobStatusEvent.from_status(
|
|
199
|
+
status=JobStatus.error.value,
|
|
200
|
+
message=f"Failed to stream job events: {exc}",
|
|
201
|
+
).format()
|
|
202
|
+
await stream.send(error_event)
|
|
203
|
+
finally:
|
|
204
|
+
await stream.eof()
|
|
205
|
+
|
|
206
|
+
return stream
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
# This exception handler only applies before stream.respond() is called
|
|
209
|
+
capture_exception_with_context(
|
|
210
|
+
exc,
|
|
211
|
+
"bot_builder_service.job_events.unexpected_error",
|
|
212
|
+
extra={"job_id": job_id},
|
|
213
|
+
tags={"endpoint": "/api/job-events/<job_id>"},
|
|
214
|
+
)
|
|
215
|
+
return response.json(
|
|
216
|
+
ApiErrorResponse(
|
|
217
|
+
error="Failed to stream job events", details={"error": str(exc)}
|
|
218
|
+
).model_dump(),
|
|
219
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
113
223
|
@bp.route("/prompt-to-bot", methods=["POST"])
|
|
114
224
|
@openapi.summary("Generate bot from natural language prompt")
|
|
115
225
|
@openapi.description(
|
|
116
|
-
"Creates a complete conversational AI bot from a natural language prompt "
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
226
|
+
"Creates a complete conversational AI bot from a natural language prompt. "
|
|
227
|
+
"Returns immediately with a job ID. Connect to `/job-events/<job_id>` to "
|
|
228
|
+
"receive server-sent events (SSE) for real-time progress tracking "
|
|
229
|
+
"throughout the bot creation process.\n\n"
|
|
230
|
+
"**SSE Event Flow** (via `/job-events/<job_id>`):\n"
|
|
120
231
|
"1. `received` - Request received by server\n"
|
|
121
232
|
"2. `generating` - Generating bot project files\n"
|
|
122
233
|
"3. `generation_success` - Bot generation completed successfully\n"
|
|
123
234
|
"4. `training` - Training the bot model\n"
|
|
124
235
|
"5. `train_success` - Model training completed\n"
|
|
125
236
|
"6. `done` - Bot creation completed\n\n"
|
|
126
|
-
"**Error Events
|
|
237
|
+
"**Error Events:**\n"
|
|
127
238
|
"- `generation_error` - Failed to generate bot from prompt\n"
|
|
128
239
|
"- `train_error` - Bot generated but training failed\n"
|
|
129
240
|
"- `validation_error` - Generated bot configuration is invalid\n"
|
|
130
241
|
"- `error` - Unexpected error occurred\n\n"
|
|
131
|
-
"**Usage
|
|
132
|
-
"
|
|
242
|
+
"**Usage:**\n"
|
|
243
|
+
"1. Send POST request with Content-Type: application/json\n"
|
|
244
|
+
"2. The response will be a JSON object `{job_id: ...}`\n"
|
|
245
|
+
"3. Connect to `/job-events/<job_id>` for a server-sent event stream of progress."
|
|
133
246
|
)
|
|
134
247
|
@openapi.tag("bot-generation")
|
|
135
248
|
@openapi.body(
|
|
136
249
|
{"application/json": model_to_schema(PromptRequest)},
|
|
137
|
-
description="Prompt request with natural language description
|
|
138
|
-
"for tracking",
|
|
250
|
+
description="Prompt request with natural language description.",
|
|
139
251
|
required=True,
|
|
140
252
|
)
|
|
141
253
|
@openapi.response(
|
|
142
254
|
200,
|
|
143
|
-
{"
|
|
144
|
-
description="
|
|
255
|
+
{"application/json": model_to_schema(JobCreateResponse)},
|
|
256
|
+
description="Job created. Poll or subscribe to /job-events/<job_id> for progress.",
|
|
145
257
|
)
|
|
146
258
|
@openapi.response(
|
|
147
259
|
400,
|
|
@@ -153,164 +265,72 @@ async def health(request: Request) -> HTTPResponse:
|
|
|
153
265
|
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
154
266
|
description="Internal server error",
|
|
155
267
|
)
|
|
156
|
-
async def handle_prompt_to_bot(request: Request) ->
|
|
268
|
+
async def handle_prompt_to_bot(request: Request) -> HTTPResponse:
|
|
157
269
|
"""Handle prompt-to-bot generation requests."""
|
|
158
|
-
sse_response = await request.respond(content_type="text/event-stream")
|
|
159
|
-
project_generator = get_project_generator(request)
|
|
160
|
-
input_channel = get_input_channel(request)
|
|
161
|
-
|
|
162
270
|
try:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
prompt_data = PromptRequest(**request.json)
|
|
171
|
-
|
|
172
|
-
# 2. Generating
|
|
173
|
-
await _send_sse_event(
|
|
174
|
-
sse_response,
|
|
175
|
-
ServerSentEvent(event="generating", data={"status": "generating"}),
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
try:
|
|
179
|
-
# Generate project with retries
|
|
180
|
-
bot_files = await project_generator.generate_project_with_retries(
|
|
181
|
-
prompt_data.prompt,
|
|
182
|
-
template=ProjectTemplateName.PLAIN,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
await _send_sse_event(
|
|
186
|
-
sse_response,
|
|
187
|
-
ServerSentEvent(
|
|
188
|
-
event="generation_success",
|
|
189
|
-
data={"status": "generation_success"},
|
|
190
|
-
),
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
except (ProjectGenerationError, LLMGenerationError) as e:
|
|
194
|
-
await _send_sse_event(
|
|
195
|
-
sse_response,
|
|
196
|
-
ServerSentEvent(
|
|
197
|
-
event="generation_error",
|
|
198
|
-
data={"status": "generation_error", "error": str(e)},
|
|
199
|
-
),
|
|
200
|
-
)
|
|
201
|
-
await sse_response.eof()
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
# 3. Training
|
|
205
|
-
await _send_sse_event(
|
|
206
|
-
sse_response,
|
|
207
|
-
ServerSentEvent(event="training", data={"status": "training"}),
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
# Train and load agent
|
|
212
|
-
importer = project_generator._create_importer()
|
|
213
|
-
request.app.ctx.agent = await train_and_load_agent(importer)
|
|
214
|
-
|
|
215
|
-
# Update input channel with new agent
|
|
216
|
-
input_channel.agent = request.app.ctx.agent
|
|
217
|
-
|
|
218
|
-
await _send_sse_event(
|
|
219
|
-
sse_response,
|
|
220
|
-
ServerSentEvent(
|
|
221
|
-
event="train_success", data={"status": "train_success"}
|
|
222
|
-
),
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
except TrainingError as e:
|
|
226
|
-
await _send_sse_event(
|
|
227
|
-
sse_response,
|
|
228
|
-
ServerSentEvent(
|
|
229
|
-
event="train_error",
|
|
230
|
-
data={"status": "train_error", "error": str(e)},
|
|
231
|
-
),
|
|
232
|
-
)
|
|
233
|
-
await sse_response.eof()
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
# 4. Done
|
|
237
|
-
await _send_sse_event(
|
|
238
|
-
sse_response,
|
|
239
|
-
ServerSentEvent(
|
|
240
|
-
event="done",
|
|
241
|
-
data={
|
|
242
|
-
"status": "done",
|
|
243
|
-
},
|
|
244
|
-
),
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
structlogger.info(
|
|
248
|
-
"bot_builder_service.prompt_to_bot.success",
|
|
249
|
-
client_id=prompt_data.client_id,
|
|
250
|
-
files_generated=list(bot_files.keys()),
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
except ValidationError as e:
|
|
254
|
-
structlogger.error(
|
|
255
|
-
"bot_builder_service.prompt_to_bot.validation_error", error=str(e)
|
|
256
|
-
)
|
|
257
|
-
await _send_sse_event(
|
|
258
|
-
sse_response,
|
|
259
|
-
ServerSentEvent(
|
|
260
|
-
event="validation_error",
|
|
261
|
-
data={"status": "validation_error", "error": str(e)},
|
|
262
|
-
),
|
|
271
|
+
payload = PromptRequest(**request.json)
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
return response.json(
|
|
274
|
+
ApiErrorResponse(
|
|
275
|
+
error="Invalid request", details={"error": str(exc)}
|
|
276
|
+
).model_dump(),
|
|
277
|
+
status=400,
|
|
263
278
|
)
|
|
264
279
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
try:
|
|
281
|
+
# Allocate job and schedule background task
|
|
282
|
+
job = job_manager.create_job()
|
|
283
|
+
request.app.add_task(run_prompt_to_bot_job(request.app, job, payload.prompt))
|
|
284
|
+
return response.json(JobCreateResponse(job_id=job.id).model_dump(), status=200)
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
capture_exception_with_context(
|
|
287
|
+
exc,
|
|
288
|
+
"bot_builder_service.prompt_to_bot.unexpected_error",
|
|
289
|
+
tags={"endpoint": "/api/prompt-to-bot"},
|
|
268
290
|
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
291
|
+
return response.json(
|
|
292
|
+
ApiErrorResponse(
|
|
293
|
+
error="Failed to create prompt-to-bot job",
|
|
294
|
+
details={"error": str(exc)},
|
|
295
|
+
).model_dump(),
|
|
296
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
272
297
|
)
|
|
273
|
-
finally:
|
|
274
|
-
await sse_response.eof()
|
|
275
298
|
|
|
276
299
|
|
|
277
300
|
@bp.route("/template-to-bot", methods=["POST"])
|
|
278
301
|
@openapi.summary("Generate bot from predefined template")
|
|
279
302
|
@openapi.description(
|
|
280
|
-
"Creates a complete conversational AI bot from a predefined template
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
303
|
+
"Creates a complete conversational AI bot from a predefined template. "
|
|
304
|
+
"Returns immediately with a job ID. Connect to `/job-events/<job_id>` to "
|
|
305
|
+
"receive server-sent events (SSE) for real-time progress tracking "
|
|
306
|
+
"throughout the bot creation process.\n\n"
|
|
307
|
+
"**SSE Event Flow** (via `/job-events/<job_id>`):\n"
|
|
284
308
|
"1. `received` - Request received by server\n"
|
|
285
309
|
"2. `generating` - Initializing bot from template\n"
|
|
286
310
|
"3. `generation_success` - Template initialization completed successfully\n"
|
|
287
311
|
"4. `training` - Training the bot model\n"
|
|
288
312
|
"5. `train_success` - Model training completed\n"
|
|
289
313
|
"6. `done` - Bot creation completed\n\n"
|
|
290
|
-
"**Error Events
|
|
314
|
+
"**Error Events:**\n"
|
|
291
315
|
"- `generation_error` - Failed to initialize bot from template\n"
|
|
292
316
|
"- `train_error` - Template loaded but training failed\n"
|
|
293
317
|
"- `validation_error` - Template configuration is invalid\n"
|
|
294
318
|
"- `error` - Unexpected error occurred\n\n"
|
|
295
|
-
"**Usage
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
319
|
+
"**Usage:**\n"
|
|
320
|
+
"1. Send POST request with Content-Type: application/json\n"
|
|
321
|
+
"2. The response will be a JSON object `{job_id: ...}`\n"
|
|
322
|
+
"3. Connect to `/job-events/<job_id>` for a server-sent event stream of progress."
|
|
299
323
|
)
|
|
300
324
|
@openapi.tag("bot-generation")
|
|
301
325
|
@openapi.body(
|
|
302
326
|
{"application/json": model_to_schema(TemplateRequest)},
|
|
303
|
-
description="Template request with template name
|
|
327
|
+
description="Template request with template name.",
|
|
304
328
|
required=True,
|
|
305
329
|
)
|
|
306
330
|
@openapi.response(
|
|
307
331
|
200,
|
|
308
|
-
{"
|
|
309
|
-
description="
|
|
310
|
-
example=ServerSentEvent(
|
|
311
|
-
event="generation_success",
|
|
312
|
-
data={"status": "generation_success"},
|
|
313
|
-
).model_dump(),
|
|
332
|
+
{"application/json": model_to_schema(JobCreateResponse)},
|
|
333
|
+
description="Job created. Poll or subscribe to /job-events/<job_id> for progress.",
|
|
314
334
|
)
|
|
315
335
|
@openapi.response(
|
|
316
336
|
400,
|
|
@@ -322,125 +342,38 @@ async def handle_prompt_to_bot(request: Request) -> None:
|
|
|
322
342
|
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
323
343
|
description="Internal server error",
|
|
324
344
|
)
|
|
325
|
-
async def handle_template_to_bot(request: Request) ->
|
|
326
|
-
"""
|
|
327
|
-
sse_response = await request.respond(content_type="text/event-stream")
|
|
328
|
-
project_generator = get_project_generator(request)
|
|
329
|
-
input_channel = get_input_channel(request)
|
|
330
|
-
|
|
345
|
+
async def handle_template_to_bot(request: Request) -> HTTPResponse:
|
|
346
|
+
"""Create a new template-to-bot job and return job_id immediately."""
|
|
331
347
|
try:
|
|
332
|
-
# 1. Received
|
|
333
|
-
await _send_sse_event(
|
|
334
|
-
sse_response,
|
|
335
|
-
ServerSentEvent(event="received", data={"status": "received"}),
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
# Validate request
|
|
339
348
|
template_data = TemplateRequest(**request.json)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
try:
|
|
348
|
-
# Generate project with retries
|
|
349
|
-
project_generator.init_from_template(
|
|
350
|
-
template_data.template_name,
|
|
351
|
-
)
|
|
352
|
-
bot_files = project_generator.get_bot_files()
|
|
353
|
-
|
|
354
|
-
await _send_sse_event(
|
|
355
|
-
sse_response,
|
|
356
|
-
ServerSentEvent(
|
|
357
|
-
event="generation_success",
|
|
358
|
-
data={"status": "generation_success"},
|
|
359
|
-
),
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
except ProjectGenerationError as e:
|
|
363
|
-
await _send_sse_event(
|
|
364
|
-
sse_response,
|
|
365
|
-
ServerSentEvent(
|
|
366
|
-
event="generation_error",
|
|
367
|
-
data={"status": "generation_error", "error": str(e)},
|
|
368
|
-
),
|
|
369
|
-
)
|
|
370
|
-
await sse_response.eof()
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
# 3. Training
|
|
374
|
-
await _send_sse_event(
|
|
375
|
-
sse_response,
|
|
376
|
-
ServerSentEvent(event="training", data={"status": "training"}),
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
try:
|
|
380
|
-
# Train and load agent
|
|
381
|
-
importer = project_generator._create_importer()
|
|
382
|
-
request.app.ctx.agent = await train_and_load_agent(importer)
|
|
383
|
-
|
|
384
|
-
# Update input channel with new agent
|
|
385
|
-
input_channel.agent = request.app.ctx.agent
|
|
386
|
-
|
|
387
|
-
await _send_sse_event(
|
|
388
|
-
sse_response,
|
|
389
|
-
ServerSentEvent(
|
|
390
|
-
event="train_success", data={"status": "train_success"}
|
|
391
|
-
),
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
except TrainingError as e:
|
|
395
|
-
await _send_sse_event(
|
|
396
|
-
sse_response,
|
|
397
|
-
ServerSentEvent(
|
|
398
|
-
event="train_error",
|
|
399
|
-
data={"status": "train_error", "error": str(e)},
|
|
400
|
-
),
|
|
401
|
-
)
|
|
402
|
-
await sse_response.eof()
|
|
403
|
-
return
|
|
404
|
-
|
|
405
|
-
# 4. Done
|
|
406
|
-
await _send_sse_event(
|
|
407
|
-
sse_response,
|
|
408
|
-
ServerSentEvent(
|
|
409
|
-
event="done",
|
|
410
|
-
data={
|
|
411
|
-
"status": "done",
|
|
412
|
-
},
|
|
413
|
-
),
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
structlogger.info(
|
|
417
|
-
"bot_builder_service.template_to_bot.success",
|
|
418
|
-
client_id=template_data.client_id,
|
|
419
|
-
files_generated=list(bot_files.keys()),
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
return response.json(
|
|
351
|
+
ApiErrorResponse(
|
|
352
|
+
error="Invalid request", details={"error": str(exc)}
|
|
353
|
+
).model_dump(),
|
|
354
|
+
status=400,
|
|
420
355
|
)
|
|
421
356
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
sse_response,
|
|
428
|
-
ServerSentEvent(
|
|
429
|
-
event="validation_error",
|
|
430
|
-
data={"status": "validation_error", "error": str(e)},
|
|
431
|
-
),
|
|
357
|
+
try:
|
|
358
|
+
# allocate job and schedule background task
|
|
359
|
+
job = job_manager.create_job()
|
|
360
|
+
request.app.add_task(
|
|
361
|
+
run_template_to_bot_job(request.app, job, template_data.template_name)
|
|
432
362
|
)
|
|
433
|
-
|
|
434
|
-
except Exception as
|
|
435
|
-
|
|
436
|
-
|
|
363
|
+
return response.json(JobCreateResponse(job_id=job.id).model_dump(), status=200)
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
capture_exception_with_context(
|
|
366
|
+
exc,
|
|
367
|
+
"bot_builder_service.template_to_bot.unexpected_error",
|
|
368
|
+
tags={"endpoint": "/api/template-to-bot"},
|
|
437
369
|
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
370
|
+
return response.json(
|
|
371
|
+
ApiErrorResponse(
|
|
372
|
+
error="Failed to create template-to-bot job",
|
|
373
|
+
details={"error": str(exc)},
|
|
374
|
+
).model_dump(),
|
|
375
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
441
376
|
)
|
|
442
|
-
finally:
|
|
443
|
-
await sse_response.eof()
|
|
444
377
|
|
|
445
378
|
|
|
446
379
|
@bp.route("/files", methods=["GET"])
|
|
@@ -459,18 +392,32 @@ async def handle_template_to_bot(request: Request) -> None:
|
|
|
459
392
|
)
|
|
460
393
|
async def get_bot_files(request: Request) -> HTTPResponse:
|
|
461
394
|
"""Get current bot files."""
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
395
|
+
try:
|
|
396
|
+
project_generator = get_project_generator(request)
|
|
397
|
+
bot_files = project_generator.get_bot_files()
|
|
398
|
+
return response.json(bot_files)
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
capture_exception_with_context(
|
|
401
|
+
exc,
|
|
402
|
+
"bot_builder_service.get_bot_files.unexpected_error",
|
|
403
|
+
tags={"endpoint": "/api/files", "method": "GET"},
|
|
404
|
+
)
|
|
405
|
+
return response.json(
|
|
406
|
+
ApiErrorResponse(
|
|
407
|
+
error="Failed to retrieve bot files", details={"error": str(exc)}
|
|
408
|
+
).model_dump(),
|
|
409
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
410
|
+
)
|
|
465
411
|
|
|
466
412
|
|
|
467
413
|
@bp.route("/files", methods=["PUT"])
|
|
468
414
|
@openapi.summary("Update bot files")
|
|
469
415
|
@openapi.description(
|
|
470
416
|
"Updates the bot configuration files and retrains the model. "
|
|
471
|
-
"Returns
|
|
472
|
-
"
|
|
473
|
-
"
|
|
417
|
+
"Returns immediately with a job ID. Connect to `/job-events/<job_id>` "
|
|
418
|
+
"for real-time SSE progress tracking."
|
|
419
|
+
"\n\n"
|
|
420
|
+
"**SSE Event Flow:** (available via /job-events/<job_id>)\n"
|
|
474
421
|
"1. `received` - Request received by server\n"
|
|
475
422
|
"2. `validating` - Validating bot configuration files\n"
|
|
476
423
|
"3. `validation_success` - File validation completed successfully\n"
|
|
@@ -481,8 +428,10 @@ async def get_bot_files(request: Request) -> HTTPResponse:
|
|
|
481
428
|
"- `validation_error` - Bot configuration files are invalid\n"
|
|
482
429
|
"- `train_error` - Files updated but training failed\n"
|
|
483
430
|
"- `error` - Unexpected error occurred\n\n"
|
|
484
|
-
"**Usage
|
|
485
|
-
"
|
|
431
|
+
"**Usage:**\n"
|
|
432
|
+
"1. Send PUT request with Content-Type: application/json\n"
|
|
433
|
+
"2. The response will be a JSON object `{job_id: ...}`\n"
|
|
434
|
+
"3. Connect to `/job-events/<job_id>` for a server-sent event stream of progress."
|
|
486
435
|
)
|
|
487
436
|
@openapi.tag("bot-files")
|
|
488
437
|
@openapi.body(
|
|
@@ -494,8 +443,11 @@ async def get_bot_files(request: Request) -> HTTPResponse:
|
|
|
494
443
|
)
|
|
495
444
|
@openapi.response(
|
|
496
445
|
200,
|
|
497
|
-
{"
|
|
498
|
-
description=
|
|
446
|
+
{"application/json": model_to_schema(JobCreateResponse)},
|
|
447
|
+
description=(
|
|
448
|
+
"Job created. Poll or subscribe to /job-events/<job_id> "
|
|
449
|
+
"for progress and SSE updates."
|
|
450
|
+
),
|
|
499
451
|
)
|
|
500
452
|
@openapi.response(
|
|
501
453
|
400,
|
|
@@ -507,101 +459,34 @@ async def get_bot_files(request: Request) -> HTTPResponse:
|
|
|
507
459
|
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
508
460
|
description="Internal server error",
|
|
509
461
|
)
|
|
510
|
-
async def update_bot_files(request: Request) ->
|
|
462
|
+
async def update_bot_files(request: Request) -> HTTPResponse:
|
|
511
463
|
"""Update bot files with server-sent events for progress tracking."""
|
|
512
|
-
sse_response = await request.respond(content_type="text/event-stream")
|
|
513
|
-
project_generator = get_project_generator(request)
|
|
514
|
-
input_channel = get_input_channel(request)
|
|
515
|
-
|
|
516
464
|
try:
|
|
517
|
-
# 1. Received
|
|
518
|
-
await _send_sse_event(
|
|
519
|
-
sse_response,
|
|
520
|
-
ServerSentEvent(event="received", data={"status": "received"}),
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
# Update bot files
|
|
524
465
|
bot_files = request.json
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
)
|
|
532
|
-
|
|
533
|
-
try:
|
|
534
|
-
importer = project_generator._create_importer()
|
|
535
|
-
validation_error = await validate_project(importer)
|
|
536
|
-
|
|
537
|
-
if validation_error:
|
|
538
|
-
raise ValidationError(validation_error)
|
|
539
|
-
|
|
540
|
-
await _send_sse_event(
|
|
541
|
-
sse_response,
|
|
542
|
-
ServerSentEvent(
|
|
543
|
-
event="validation_success",
|
|
544
|
-
data={"status": "validation_success"},
|
|
545
|
-
),
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
except ValidationError as e:
|
|
549
|
-
await _send_sse_event(
|
|
550
|
-
sse_response,
|
|
551
|
-
ServerSentEvent(
|
|
552
|
-
event="validation_error",
|
|
553
|
-
data={"status": "validation_error", "error": str(e)},
|
|
554
|
-
),
|
|
555
|
-
)
|
|
556
|
-
await sse_response.eof()
|
|
557
|
-
return
|
|
558
|
-
|
|
559
|
-
# 3. Training
|
|
560
|
-
await _send_sse_event(
|
|
561
|
-
sse_response,
|
|
562
|
-
ServerSentEvent(event="training", data={"status": "training"}),
|
|
466
|
+
except Exception as exc:
|
|
467
|
+
return response.json(
|
|
468
|
+
ApiErrorResponse(
|
|
469
|
+
error="Invalid request", details={"error": str(exc)}
|
|
470
|
+
).model_dump(),
|
|
471
|
+
status=400,
|
|
563
472
|
)
|
|
564
473
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
except TrainingError as e:
|
|
577
|
-
await _send_sse_event(
|
|
578
|
-
sse_response,
|
|
579
|
-
ServerSentEvent(
|
|
580
|
-
event="train_error",
|
|
581
|
-
data={"status": "train_error", "error": str(e)},
|
|
582
|
-
),
|
|
583
|
-
)
|
|
584
|
-
await sse_response.eof()
|
|
585
|
-
return
|
|
586
|
-
|
|
587
|
-
# 4. Done
|
|
588
|
-
await _send_sse_event(
|
|
589
|
-
sse_response,
|
|
590
|
-
ServerSentEvent(
|
|
591
|
-
event="done",
|
|
592
|
-
data={
|
|
593
|
-
"status": "done",
|
|
594
|
-
},
|
|
595
|
-
),
|
|
474
|
+
try:
|
|
475
|
+
job = job_manager.create_job()
|
|
476
|
+
request.app.add_task(run_update_files_job(request.app, job, bot_files))
|
|
477
|
+
return response.json(JobCreateResponse(job_id=job.id).model_dump(), status=200)
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
capture_exception_with_context(
|
|
480
|
+
exc,
|
|
481
|
+
"bot_builder_service.update_bot_files.unexpected_error",
|
|
482
|
+
tags={"endpoint": "/api/files", "method": "PUT"},
|
|
596
483
|
)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
484
|
+
return response.json(
|
|
485
|
+
ApiErrorResponse(
|
|
486
|
+
error="Failed to update bot files", details={"error": str(exc)}
|
|
487
|
+
).model_dump(),
|
|
488
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
602
489
|
)
|
|
603
|
-
finally:
|
|
604
|
-
await sse_response.eof()
|
|
605
490
|
|
|
606
491
|
|
|
607
492
|
@bp.route("/data", methods=["GET"])
|
|
@@ -613,9 +498,14 @@ async def update_bot_files(request: Request) -> None:
|
|
|
613
498
|
@openapi.tag("bot-data")
|
|
614
499
|
@openapi.response(
|
|
615
500
|
200,
|
|
616
|
-
{"application/json": model_to_schema(
|
|
501
|
+
{"application/json": model_to_schema(BotData)},
|
|
617
502
|
description="Bot data retrieved successfully",
|
|
618
503
|
)
|
|
504
|
+
@openapi.response(
|
|
505
|
+
409,
|
|
506
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
507
|
+
description="Agent not ready",
|
|
508
|
+
)
|
|
619
509
|
@openapi.response(
|
|
620
510
|
500,
|
|
621
511
|
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
@@ -624,37 +514,282 @@ async def update_bot_files(request: Request) -> None:
|
|
|
624
514
|
async def get_bot_data(request: Request) -> HTTPResponse:
|
|
625
515
|
"""Get current bot data in CALM import format."""
|
|
626
516
|
try:
|
|
627
|
-
|
|
628
|
-
|
|
517
|
+
agent: Optional[Agent] = request.app.ctx.agent
|
|
518
|
+
if not agent:
|
|
519
|
+
raise AgentNotReady(
|
|
520
|
+
"Can't retrieve the data without an agent being loaded."
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
bot_data = await extract_bot_data_from_agent(agent)
|
|
629
524
|
|
|
630
|
-
return response.json(
|
|
631
|
-
except
|
|
632
|
-
|
|
525
|
+
return response.json(bot_data.model_dump())
|
|
526
|
+
except AgentNotReady as e:
|
|
527
|
+
return response.json(
|
|
528
|
+
ApiErrorResponse(
|
|
529
|
+
error="Agent not ready",
|
|
530
|
+
details={"error": str(e)},
|
|
531
|
+
).model_dump(),
|
|
532
|
+
status=HTTPStatus.CONFLICT,
|
|
533
|
+
)
|
|
534
|
+
except Exception as exc:
|
|
535
|
+
capture_exception_with_context(
|
|
536
|
+
exc,
|
|
537
|
+
"bot_builder_service.get_bot_data.unexpected_error",
|
|
538
|
+
tags={"endpoint": "/api/data"},
|
|
539
|
+
)
|
|
633
540
|
return response.json(
|
|
634
541
|
ApiErrorResponse(
|
|
635
542
|
error="Failed to retrieve bot data",
|
|
543
|
+
details={"error": str(exc)},
|
|
544
|
+
).model_dump(),
|
|
545
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@bp.route("/assistant", methods=["GET"])
|
|
550
|
+
@openapi.summary("Get assistant info")
|
|
551
|
+
@openapi.description(
|
|
552
|
+
"Returns basic information about the loaded assistant, including the assistant id "
|
|
553
|
+
"as configured in the model's metadata (from config.yml)."
|
|
554
|
+
)
|
|
555
|
+
@openapi.tag("assistant")
|
|
556
|
+
@openapi.response(
|
|
557
|
+
200,
|
|
558
|
+
{"application/json": model_to_schema(AssistantInfo)},
|
|
559
|
+
description="Assistant info retrieved successfully",
|
|
560
|
+
)
|
|
561
|
+
@openapi.response(
|
|
562
|
+
409,
|
|
563
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
564
|
+
description="Agent not ready",
|
|
565
|
+
)
|
|
566
|
+
@openapi.response(
|
|
567
|
+
500,
|
|
568
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
569
|
+
description="Internal server error",
|
|
570
|
+
)
|
|
571
|
+
async def get_bot_info(request: Request) -> HTTPResponse:
|
|
572
|
+
"""Return assistant info including assistant id from model metadata."""
|
|
573
|
+
try:
|
|
574
|
+
agent: Optional[Agent] = request.app.ctx.agent
|
|
575
|
+
if not agent:
|
|
576
|
+
raise AgentNotReady("Can't retrieve bot info without a loaded agent.")
|
|
577
|
+
|
|
578
|
+
assistant_id: Optional[str] = (
|
|
579
|
+
agent.processor.model_metadata.assistant_id
|
|
580
|
+
if agent.processor
|
|
581
|
+
and agent.processor.model_metadata
|
|
582
|
+
and hasattr(agent.processor.model_metadata, "assistant_id")
|
|
583
|
+
else None
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return response.json(AssistantInfo(assistant_id=assistant_id).model_dump())
|
|
587
|
+
except AgentNotReady as e:
|
|
588
|
+
return response.json(
|
|
589
|
+
ApiErrorResponse(
|
|
590
|
+
error="Agent not ready",
|
|
636
591
|
details={"error": str(e)},
|
|
637
592
|
).model_dump(),
|
|
593
|
+
status=HTTPStatus.CONFLICT,
|
|
594
|
+
)
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
capture_exception_with_context(
|
|
597
|
+
exc,
|
|
598
|
+
"bot_builder_service.get_bot_info.unexpected_error",
|
|
599
|
+
tags={"endpoint": "/api/assistant"},
|
|
600
|
+
)
|
|
601
|
+
return response.json(
|
|
602
|
+
ApiErrorResponse(
|
|
603
|
+
error="Failed to retrieve bot info",
|
|
604
|
+
details={"error": str(exc)},
|
|
605
|
+
).model_dump(),
|
|
606
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@bp.route("/download", methods=["GET"])
|
|
611
|
+
@openapi.summary("Download bot project as tar.gz")
|
|
612
|
+
@openapi.description(
|
|
613
|
+
"Downloads the current bot project files as a compressed tar.gz archive. "
|
|
614
|
+
"Includes all configuration files and a .env file with RASA_PRO_LICENSE. "
|
|
615
|
+
"Requires valid JWT token in Authorization header."
|
|
616
|
+
)
|
|
617
|
+
@openapi.tag("bot-files")
|
|
618
|
+
@openapi.parameter(
|
|
619
|
+
"Authorization",
|
|
620
|
+
description="Bearer token for authentication",
|
|
621
|
+
_in="header",
|
|
622
|
+
required=True,
|
|
623
|
+
schema=str,
|
|
624
|
+
)
|
|
625
|
+
@openapi.parameter(
|
|
626
|
+
"project_name",
|
|
627
|
+
description="Name of the project for the archive filename and pyproject.toml",
|
|
628
|
+
_in="query",
|
|
629
|
+
required=False,
|
|
630
|
+
schema=str,
|
|
631
|
+
)
|
|
632
|
+
@openapi.response(
|
|
633
|
+
200,
|
|
634
|
+
{"application/gzip": bytes},
|
|
635
|
+
description="Bot project downloaded successfully as tar.gz",
|
|
636
|
+
)
|
|
637
|
+
@openapi.response(
|
|
638
|
+
401,
|
|
639
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
640
|
+
description="Authentication failed - invalid or missing token",
|
|
641
|
+
)
|
|
642
|
+
@openapi.response(
|
|
643
|
+
500,
|
|
644
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
645
|
+
description="Internal server error",
|
|
646
|
+
)
|
|
647
|
+
async def download_bot_project(request: Request) -> HTTPResponse:
|
|
648
|
+
"""Download bot project as tar.gz archive."""
|
|
649
|
+
try:
|
|
650
|
+
# Extract and verify token
|
|
651
|
+
auth_header = request.headers.get("Authorization", "")
|
|
652
|
+
verification_result = extract_and_verify_auth0_token(auth_header)
|
|
653
|
+
|
|
654
|
+
if verification_result.error_message is not None:
|
|
655
|
+
structlogger.error(
|
|
656
|
+
"bot_builder_service.download_bot_project.token_verification_failed",
|
|
657
|
+
error=verification_result.error_message,
|
|
658
|
+
)
|
|
659
|
+
return response.json(
|
|
660
|
+
ApiErrorResponse(
|
|
661
|
+
error=verification_result.error_message,
|
|
662
|
+
details={"expected": "Bearer <valid_token>"},
|
|
663
|
+
).model_dump(),
|
|
664
|
+
status=401,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Get bot files
|
|
668
|
+
project_generator = get_project_generator(request)
|
|
669
|
+
bot_files = project_generator.get_bot_files()
|
|
670
|
+
|
|
671
|
+
# Get project name from query parameters, default to "bot-project"
|
|
672
|
+
project_name = request.args.get("project_name", "bot-project")
|
|
673
|
+
|
|
674
|
+
# Create tar.gz archive
|
|
675
|
+
tar_data = create_bot_project_archive(bot_files, project_name)
|
|
676
|
+
|
|
677
|
+
structlogger.info(
|
|
678
|
+
"bot_builder_service.download_bot_project.success",
|
|
679
|
+
user_sub=verification_result.payload.get("sub")
|
|
680
|
+
if verification_result.payload
|
|
681
|
+
else None,
|
|
682
|
+
files_count=len(bot_files),
|
|
683
|
+
archive_size=len(tar_data),
|
|
684
|
+
payload=verification_result.payload,
|
|
685
|
+
project_name=project_name,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
return response.raw(
|
|
689
|
+
tar_data,
|
|
690
|
+
content_type="application/gzip",
|
|
691
|
+
headers={
|
|
692
|
+
"Content-Disposition": f"attachment; filename={project_name}.tar.gz"
|
|
693
|
+
},
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
except Exception as exc:
|
|
697
|
+
capture_exception_with_context(
|
|
698
|
+
exc,
|
|
699
|
+
"bot_builder_service.download_bot_project.unexpected_error",
|
|
700
|
+
tags={"endpoint": "/api/download"},
|
|
701
|
+
)
|
|
702
|
+
return response.json(
|
|
703
|
+
ApiErrorResponse(
|
|
704
|
+
error="Failed to create bot project archive",
|
|
705
|
+
details={"error": str(exc)},
|
|
706
|
+
).model_dump(),
|
|
638
707
|
status=500,
|
|
639
708
|
)
|
|
640
709
|
|
|
641
710
|
|
|
642
|
-
@bp.route("/
|
|
643
|
-
@openapi.summary("
|
|
711
|
+
@bp.route("/copilot", methods=["POST"])
|
|
712
|
+
@openapi.summary("AI copilot for bot building")
|
|
644
713
|
@openapi.description(
|
|
645
|
-
"Provides LLM-powered assistance
|
|
646
|
-
"
|
|
714
|
+
"Provides LLM-powered copilot assistance with streaming markdown responses. "
|
|
715
|
+
"Returns server-sent events (SSE) for real-time streaming of copilot responses.\n\n"
|
|
716
|
+
"The event's `event` field is the type of event. For this endpoint it will always "
|
|
717
|
+
"be: `copilot_response`.\n\n"
|
|
647
718
|
)
|
|
648
|
-
@openapi.tag("
|
|
719
|
+
@openapi.tag("copilot")
|
|
649
720
|
@openapi.body(
|
|
650
|
-
{"application/json": model_to_schema(
|
|
651
|
-
description=
|
|
721
|
+
{"application/json": model_to_schema(CopilotRequest)},
|
|
722
|
+
description=(
|
|
723
|
+
"Copilot request containing: "
|
|
724
|
+
"1. conversation history between user and copilot, "
|
|
725
|
+
"2. session ID for tracking conversation context with the bot being built."
|
|
726
|
+
),
|
|
652
727
|
required=True,
|
|
653
728
|
)
|
|
654
729
|
@openapi.response(
|
|
655
730
|
200,
|
|
656
|
-
{"
|
|
657
|
-
description=
|
|
731
|
+
{"text/event-stream": model_to_schema(ServerSentEvent)},
|
|
732
|
+
description=(
|
|
733
|
+
"Server-sent events stream with copilot responses and references. "
|
|
734
|
+
"The event's `event` field is the type "
|
|
735
|
+
"of event. For this endpoint it will always be: `copilot_response`. "
|
|
736
|
+
"The event's data field is a JSON dump. The following describes the event data "
|
|
737
|
+
"field:\n"
|
|
738
|
+
"- `response_category` can be one of the following:\n"
|
|
739
|
+
" - `copilot` - Stream token generated by the copilot.\n"
|
|
740
|
+
" - `out_of_scope_detection` - Response coming from the the out of scope detection.\n" # noqa: E501
|
|
741
|
+
" - `roleplay_detection` - Response coming from the the roleplay detection.\n"
|
|
742
|
+
" - `reference` - Reference section.\n"
|
|
743
|
+
"- `completeness`: Whether this is a streaming token or complete response.\n"
|
|
744
|
+
"- `content`: The actual response content (for streaming tokens).\n"
|
|
745
|
+
"- `references`: (Only for `reference` response category) List of reference entries. Each reference entry is a dictionary with the following keys:\n" # noqa: E501
|
|
746
|
+
" - `index`: Reference index as an integer number.\n"
|
|
747
|
+
" - `title`: Reference title as a string.\n"
|
|
748
|
+
" - `url`: Reference URL as a string.\n\n"
|
|
749
|
+
),
|
|
750
|
+
examples={
|
|
751
|
+
"Streaming token from copilot": {
|
|
752
|
+
"summary": "Streaming token from copilot",
|
|
753
|
+
"value": GeneratedContent(
|
|
754
|
+
content="<token generated by the copilot>",
|
|
755
|
+
response_category=ResponseCategory.COPILOT,
|
|
756
|
+
response_completeness=ResponseCompleteness.TOKEN,
|
|
757
|
+
)
|
|
758
|
+
.to_sse_event()
|
|
759
|
+
.model_dump(),
|
|
760
|
+
},
|
|
761
|
+
"Complete response from out of scope / roleplay detection / guardrail policy violation": { # noqa: E501
|
|
762
|
+
"summary": "Complete response from copilot",
|
|
763
|
+
"value": GeneratedContent(
|
|
764
|
+
content="<response from the out of scope detection>",
|
|
765
|
+
response_category=ResponseCategory.OUT_OF_SCOPE_DETECTION,
|
|
766
|
+
response_completeness=ResponseCompleteness.COMPLETE,
|
|
767
|
+
)
|
|
768
|
+
.to_sse_event()
|
|
769
|
+
.model_dump(),
|
|
770
|
+
},
|
|
771
|
+
"Response with the references": {
|
|
772
|
+
"summary": "Reference section with all references",
|
|
773
|
+
"value": ReferenceSection(
|
|
774
|
+
references=[
|
|
775
|
+
ReferenceEntry(
|
|
776
|
+
index=1,
|
|
777
|
+
title="Title of the reference",
|
|
778
|
+
url="https://rasa.com/docs/...",
|
|
779
|
+
),
|
|
780
|
+
ReferenceEntry(
|
|
781
|
+
index=2,
|
|
782
|
+
title="Title of another reference",
|
|
783
|
+
url="https://rasa.com/docs/...",
|
|
784
|
+
),
|
|
785
|
+
],
|
|
786
|
+
response_category=ResponseCategory.REFERENCE,
|
|
787
|
+
response_completeness=ResponseCompleteness.COMPLETE,
|
|
788
|
+
)
|
|
789
|
+
.to_sse_event()
|
|
790
|
+
.model_dump(),
|
|
791
|
+
},
|
|
792
|
+
},
|
|
658
793
|
)
|
|
659
794
|
@openapi.response(
|
|
660
795
|
400,
|
|
@@ -664,79 +799,142 @@ async def get_bot_data(request: Request) -> HTTPResponse:
|
|
|
664
799
|
@openapi.response(
|
|
665
800
|
502,
|
|
666
801
|
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
667
|
-
description="LLM generation failed",
|
|
802
|
+
description="LLM generation failed.",
|
|
668
803
|
)
|
|
669
804
|
@openapi.response(
|
|
670
805
|
500,
|
|
671
806
|
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
672
|
-
description="Internal server error",
|
|
807
|
+
description="Internal server error.",
|
|
673
808
|
)
|
|
674
|
-
async def
|
|
675
|
-
"""Handle
|
|
809
|
+
async def copilot(request: Request) -> None:
|
|
810
|
+
"""Handle copilot requests with streaming markdown responses."""
|
|
811
|
+
sse = await request.respond(content_type="text/event-stream")
|
|
676
812
|
project_generator = get_project_generator(request)
|
|
677
|
-
input_channel = get_input_channel(request)
|
|
678
813
|
|
|
679
814
|
try:
|
|
680
|
-
# Validate
|
|
681
|
-
|
|
815
|
+
# 1. Validate and unpack input
|
|
816
|
+
req = CopilotRequest(**request.json)
|
|
682
817
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
request.
|
|
818
|
+
telemetry = CopilotTelemetry(
|
|
819
|
+
project_id=HELLO_RASA_PROJECT_ID,
|
|
820
|
+
user_id=request.headers.get(HEADER_USER_ID),
|
|
686
821
|
)
|
|
687
|
-
|
|
688
|
-
|
|
822
|
+
structlogger.debug("builder.copilot.telemetry.request.init")
|
|
823
|
+
|
|
824
|
+
if req.last_message and req.last_message.role == ROLE_USER:
|
|
825
|
+
structlogger.debug("builder.copilot.telemetry.request.user_turn")
|
|
826
|
+
# Offload telemetry logging to a background task
|
|
827
|
+
request.app.add_task(
|
|
828
|
+
asyncio.to_thread(
|
|
829
|
+
telemetry.log_user_turn, req.last_message.get_text_content()
|
|
830
|
+
)
|
|
831
|
+
)
|
|
689
832
|
|
|
690
|
-
#
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
833
|
+
# 2. Get the necessary context for the copilot
|
|
834
|
+
tracker = await current_tracker_from_input_channel(request.app, req.session_id)
|
|
835
|
+
tracker_context = TrackerContext.from_tracker(
|
|
836
|
+
tracker, max_turns=COPILOT_ASSISTANT_TRACKER_MAX_TURNS
|
|
837
|
+
)
|
|
838
|
+
tracker_context = await check_assistant_chat_for_policy_violations(
|
|
839
|
+
tracker_context=tracker_context,
|
|
840
|
+
hello_rasa_user_id=request.headers.get(HEADER_USER_ID),
|
|
841
|
+
hello_rasa_project_id=HELLO_RASA_PROJECT_ID,
|
|
842
|
+
)
|
|
843
|
+
context = CopilotContext(
|
|
844
|
+
tracker_context=tracker_context,
|
|
845
|
+
assistant_logs=get_recent_logs(),
|
|
846
|
+
assistant_files=project_generator.get_bot_files(),
|
|
847
|
+
copilot_chat_history=req.copilot_chat_history,
|
|
696
848
|
)
|
|
697
849
|
|
|
698
|
-
#
|
|
699
|
-
|
|
700
|
-
|
|
850
|
+
# 3. Run guardrail policy checks. If any policy violations are detected,
|
|
851
|
+
# send a response and end the stream.
|
|
852
|
+
guardrail_response: Optional[
|
|
853
|
+
GeneratedContent
|
|
854
|
+
] = await check_copilot_chat_for_policy_violations(
|
|
855
|
+
context=context,
|
|
856
|
+
hello_rasa_user_id=request.headers.get(HEADER_USER_ID),
|
|
857
|
+
hello_rasa_project_id=HELLO_RASA_PROJECT_ID,
|
|
858
|
+
)
|
|
859
|
+
if guardrail_response is not None:
|
|
860
|
+
await sse.send(guardrail_response.to_sse_event().format())
|
|
861
|
+
await sse.eof()
|
|
862
|
+
return
|
|
701
863
|
|
|
702
|
-
|
|
864
|
+
# 4. Get the original response stream from copilot and handle it with the
|
|
865
|
+
# copilot response handler
|
|
866
|
+
start_timestamp = time.perf_counter()
|
|
867
|
+
copilot_client = llm_service.instantiate_copilot()
|
|
868
|
+
(
|
|
869
|
+
original_stream,
|
|
870
|
+
used_documents,
|
|
871
|
+
) = await copilot_client.generate_response(context)
|
|
872
|
+
|
|
873
|
+
copilot_response_handler = llm_service.instantiate_handler(
|
|
874
|
+
COPILOT_HANDLER_ROLLING_BUFFER_SIZE
|
|
875
|
+
)
|
|
876
|
+
intercepted_stream = copilot_response_handler.handle_response(original_stream)
|
|
877
|
+
|
|
878
|
+
# 5. Stream the intercepted response
|
|
879
|
+
async for token in intercepted_stream:
|
|
880
|
+
await sse.send(token.to_sse_event().format())
|
|
881
|
+
|
|
882
|
+
# Offload telemetry logging to a background task
|
|
883
|
+
request.app.add_task(
|
|
884
|
+
asyncio.to_thread(
|
|
885
|
+
telemetry.log_copilot_from_handler,
|
|
886
|
+
handler=copilot_response_handler,
|
|
887
|
+
used_documents=used_documents,
|
|
888
|
+
latency_ms=int((time.perf_counter() - start_timestamp) * 1000),
|
|
889
|
+
**copilot_client.usage_statistics.model_dump(),
|
|
890
|
+
)
|
|
891
|
+
)
|
|
703
892
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
893
|
+
# 6. Once the stream is over, extract and send references
|
|
894
|
+
# if any documents were used
|
|
895
|
+
if used_documents:
|
|
896
|
+
reference_section = copilot_response_handler.extract_references(
|
|
897
|
+
used_documents
|
|
898
|
+
)
|
|
899
|
+
await sse.send(reference_section.to_sse_event().format())
|
|
900
|
+
|
|
901
|
+
except CopilotStreamError as e:
|
|
902
|
+
capture_exception_with_context(
|
|
903
|
+
e,
|
|
904
|
+
"bot_builder_service.copilot.generation_error",
|
|
905
|
+
extra={"session_id": req.session_id},
|
|
906
|
+
tags={"endpoint": "/api/copilot"},
|
|
707
907
|
)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
908
|
+
await sse.send(
|
|
909
|
+
ServerSentEvent(
|
|
910
|
+
event="error",
|
|
911
|
+
data={"error": str(e)},
|
|
912
|
+
).format()
|
|
713
913
|
)
|
|
714
914
|
|
|
715
|
-
except Exception as
|
|
716
|
-
|
|
717
|
-
|
|
915
|
+
except Exception as exc:
|
|
916
|
+
capture_exception_with_context(
|
|
917
|
+
exc,
|
|
918
|
+
"bot_builder_service.copilot.unexpected_error",
|
|
919
|
+
extra={"session_id": req.session_id if "req" in locals() else None},
|
|
920
|
+
tags={"endpoint": "/api/copilot"},
|
|
718
921
|
)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
).
|
|
724
|
-
status=500,
|
|
922
|
+
await sse.send(
|
|
923
|
+
ServerSentEvent(
|
|
924
|
+
event="error",
|
|
925
|
+
data={"error": str(exc)},
|
|
926
|
+
).format()
|
|
725
927
|
)
|
|
726
928
|
|
|
929
|
+
finally:
|
|
930
|
+
await sse.eof()
|
|
931
|
+
|
|
727
932
|
|
|
728
933
|
async def current_tracker_from_input_channel(
|
|
729
|
-
app: Any,
|
|
934
|
+
app: Any, session_id: str
|
|
730
935
|
) -> Optional[DialogueStateTracker]:
|
|
731
936
|
"""Generate chat bot context from current conversation."""
|
|
732
|
-
if app.ctx.agent and
|
|
733
|
-
return await app.ctx.agent.tracker_store.retrieve(
|
|
734
|
-
input_channel.latest_tracker_session_id
|
|
735
|
-
)
|
|
937
|
+
if app.ctx.agent and session_id:
|
|
938
|
+
return await app.ctx.agent.tracker_store.retrieve(session_id)
|
|
736
939
|
else:
|
|
737
940
|
return None
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
async def _send_sse_event(sse_response: HTTPResponse, event: ServerSentEvent) -> None:
|
|
741
|
-
"""Send a server-sent event."""
|
|
742
|
-
await sse_response.send(event.format())
|