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.

Files changed (273) hide show
  1. rasa/__main__.py +8 -0
  2. rasa/builder/auth.py +71 -0
  3. rasa/builder/config.py +16 -0
  4. rasa/builder/copilot/constants.py +15 -0
  5. rasa/builder/copilot/copilot.py +342 -0
  6. rasa/builder/copilot/copilot_response_handler.py +471 -0
  7. rasa/builder/copilot/exceptions.py +20 -0
  8. rasa/builder/copilot/models.py +344 -0
  9. rasa/builder/copilot/prompts/copilot_system_prompt.jinja2 +495 -0
  10. rasa/builder/copilot/telemetry.py +195 -0
  11. rasa/builder/document_retrieval/__init__.py +0 -0
  12. rasa/builder/document_retrieval/constants.py +16 -0
  13. rasa/builder/{inkeep_document_retrieval.py → document_retrieval/inkeep_document_retrieval.py} +53 -44
  14. rasa/builder/document_retrieval/models.py +62 -0
  15. rasa/builder/download.py +140 -0
  16. rasa/builder/guardrails/__init__.py +1 -0
  17. rasa/builder/guardrails/constants.py +4 -0
  18. rasa/builder/guardrails/exceptions.py +4 -0
  19. rasa/builder/guardrails/lakera.py +188 -0
  20. rasa/builder/guardrails/models.py +199 -0
  21. rasa/builder/guardrails/utils.py +305 -0
  22. rasa/builder/job_manager.py +87 -0
  23. rasa/builder/jobs.py +232 -0
  24. rasa/builder/llm_service.py +89 -173
  25. rasa/builder/logging_utils.py +162 -4
  26. rasa/builder/main.py +29 -16
  27. rasa/builder/models.py +93 -121
  28. rasa/builder/project_generator.py +91 -7
  29. rasa/builder/scrape_rasa_docs.py +1 -1
  30. rasa/builder/service.py +650 -452
  31. rasa/builder/shared/tracker_context.py +212 -0
  32. rasa/builder/validation_service.py +4 -4
  33. rasa/cli/data.py +8 -3
  34. rasa/cli/project_templates/basic/actions/action_api.py +15 -0
  35. rasa/cli/project_templates/basic/actions/action_human_handoff.py +44 -0
  36. rasa/cli/project_templates/basic/config.yml +23 -0
  37. rasa/cli/project_templates/{plain → basic}/credentials.yml +8 -7
  38. rasa/cli/project_templates/basic/data/general/feedback.yml +20 -0
  39. rasa/cli/project_templates/basic/data/general/goodbye.yml +6 -0
  40. rasa/cli/project_templates/basic/data/general/hello.yml +7 -0
  41. rasa/cli/project_templates/basic/data/general/help.yml +6 -0
  42. rasa/cli/project_templates/basic/data/general/human_handoff.yml +16 -0
  43. rasa/cli/project_templates/basic/data/general/welcome.yml +9 -0
  44. rasa/cli/project_templates/{finance/data/patterns → basic/data/system}/pattern_completed.yml +2 -1
  45. rasa/cli/project_templates/basic/data/system/pattern_correction.yml +7 -0
  46. rasa/cli/project_templates/basic/data/system/pattern_search.yml +8 -0
  47. rasa/cli/project_templates/basic/data/system/pattern_session_start.yml +8 -0
  48. rasa/cli/project_templates/basic/docs/rasa_assistant_qa.txt +65 -0
  49. rasa/cli/project_templates/basic/docs/template.txt +7 -0
  50. rasa/cli/project_templates/basic/domain/general/assistant_details.yml +12 -0
  51. rasa/cli/project_templates/basic/domain/general/bot_identity.yml +5 -0
  52. rasa/cli/project_templates/basic/domain/general/cannot_handle.yml +5 -0
  53. rasa/cli/project_templates/basic/domain/general/feedback.yml +28 -0
  54. rasa/cli/project_templates/basic/domain/general/goodbye.yml +7 -0
  55. rasa/cli/project_templates/basic/domain/general/help.yml +5 -0
  56. rasa/cli/project_templates/basic/domain/general/human_handoff_domain.yml +35 -0
  57. rasa/cli/project_templates/{finance/domain/default_actions.yml → basic/domain/general/utils.yml} +0 -3
  58. rasa/cli/project_templates/basic/domain/general/welcome.yml +7 -0
  59. rasa/cli/project_templates/{plain → basic}/endpoints.yml +42 -27
  60. rasa/cli/project_templates/basic/prompts/rephraser_demo_personality_prompt.jinja2 +19 -0
  61. rasa/cli/project_templates/defaults.py +25 -3
  62. rasa/cli/project_templates/finance/actions/__init__.py +46 -0
  63. rasa/cli/project_templates/finance/actions/accounts/__init__.py +0 -0
  64. rasa/cli/project_templates/finance/actions/{action_ask_account.py → accounts/action_ask_account.py} +6 -9
  65. rasa/cli/project_templates/finance/actions/{action_check_balance.py → accounts/action_check_balance.py} +4 -4
  66. rasa/cli/project_templates/finance/actions/action_session_start.py +11 -6
  67. rasa/cli/project_templates/finance/actions/cards/__init__.py +0 -0
  68. rasa/cli/project_templates/finance/actions/{action_ask_card.py → cards/action_ask_card.py} +4 -3
  69. rasa/cli/project_templates/finance/actions/{action_check_card_existence.py → cards/action_check_card_existence.py} +4 -3
  70. rasa/cli/project_templates/finance/actions/{action_update_card_status.py → cards/action_update_card_status.py} +18 -9
  71. rasa/cli/project_templates/finance/actions/database.py +1 -0
  72. rasa/cli/project_templates/finance/actions/transfers/__init__.py +0 -0
  73. rasa/cli/project_templates/finance/actions/{action_add_payee.py → transfers/action_add_payee.py} +8 -3
  74. rasa/cli/project_templates/finance/actions/{action_ask_account_from.py → transfers/action_ask_account_from.py} +5 -4
  75. rasa/cli/project_templates/finance/actions/{action_check_payee_existence.py → transfers/action_check_payee_existence.py} +3 -3
  76. rasa/cli/project_templates/finance/actions/{action_check_sufficient_funds.py → transfers/action_check_sufficient_funds.py} +3 -4
  77. rasa/cli/project_templates/finance/actions/{action_list_payees.py → transfers/action_list_payees.py} +4 -3
  78. rasa/cli/project_templates/finance/actions/{action_remove_payee.py → transfers/action_remove_payee.py} +4 -4
  79. rasa/cli/project_templates/finance/config.yml +8 -19
  80. rasa/cli/project_templates/finance/credentials.yml +6 -7
  81. rasa/cli/project_templates/finance/csvs/cards.csv +10 -10
  82. rasa/cli/project_templates/finance/csvs/payees.csv +10 -9
  83. rasa/cli/project_templates/finance/data/{flows → accounts}/check_balance.yml +2 -1
  84. rasa/cli/project_templates/finance/data/general/bot_identity.yml +6 -0
  85. rasa/cli/project_templates/finance/data/general/feedback.yml +20 -0
  86. rasa/cli/project_templates/finance/data/general/goodbye.yml +6 -0
  87. rasa/cli/project_templates/finance/data/general/hello.yml +7 -0
  88. rasa/cli/project_templates/finance/data/{flows/welcome.yml → general/help.yml} +2 -7
  89. rasa/cli/project_templates/finance/data/general/human_handoff.yml +16 -0
  90. rasa/cli/project_templates/finance/data/general/welcome.yml +9 -0
  91. rasa/cli/project_templates/finance/data/{patterns → system/patterns}/pattern_chitchat.yml +0 -2
  92. rasa/cli/project_templates/finance/data/system/patterns/pattern_completed.yml +7 -0
  93. rasa/cli/project_templates/finance/data/system/patterns/pattern_correction.yml +7 -0
  94. rasa/cli/project_templates/finance/data/system/patterns/pattern_search.yml +8 -0
  95. rasa/cli/project_templates/finance/data/{patterns → system/patterns}/pattern_session_start.yml +0 -1
  96. rasa/cli/project_templates/finance/domain/{check_balance.yml → accounts/check_balance.yml} +2 -0
  97. rasa/cli/project_templates/finance/domain/general/assistant_details.yml +12 -0
  98. rasa/cli/project_templates/finance/domain/general/bot_identity.yml +5 -0
  99. rasa/cli/project_templates/finance/domain/general/cannot_handle.yml +5 -0
  100. rasa/cli/project_templates/finance/domain/general/defaults.yml +24 -0
  101. rasa/cli/project_templates/finance/domain/general/feedback.yml +28 -0
  102. rasa/cli/project_templates/finance/domain/general/goodbye.yml +7 -0
  103. rasa/cli/project_templates/finance/domain/general/help.yml +5 -0
  104. rasa/cli/project_templates/finance/domain/general/human_handoff.yml +30 -0
  105. rasa/cli/project_templates/finance/domain/general/utils.yml +13 -0
  106. rasa/cli/project_templates/finance/domain/general/welcome.yml +8 -0
  107. rasa/cli/project_templates/finance/endpoints.yml +1 -0
  108. rasa/cli/project_templates/finance/prompts/rephraser_demo_personality_prompt.jinja2 +3 -3
  109. rasa/cli/project_templates/telco/actions/actions_billing.py +24 -17
  110. rasa/cli/project_templates/telco/actions/actions_get_data_from_db.py +6 -1
  111. rasa/cli/project_templates/telco/actions/actions_run_diagnostics.py +6 -1
  112. rasa/cli/project_templates/telco/actions/actions_session_start.py +6 -1
  113. rasa/cli/project_templates/tutorial/config.yml +2 -1
  114. rasa/cli/scaffold.py +27 -2
  115. rasa/cli/train.py +8 -0
  116. rasa/cli/utils.py +31 -15
  117. rasa/core/actions/action.py +28 -41
  118. rasa/core/actions/action_run_slot_rejections.py +1 -1
  119. rasa/core/channels/development_inspector.py +47 -14
  120. rasa/core/channels/inspector/dist/assets/{arc-371401b1.js → arc-1ddec37b.js} +1 -1
  121. rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-3f126156.js → blockDiagram-38ab4fdb-18af387c.js} +1 -1
  122. rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-12f22eb7.js → c4Diagram-3d4e48cf-250127a3.js} +1 -1
  123. rasa/core/channels/inspector/dist/assets/channel-59f6d54b.js +1 -0
  124. rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-03b1d386.js → classDiagram-70f12bd4-c3388b34.js} +1 -1
  125. rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-84f69d63.js → classDiagram-v2-f2320105-9c893a82.js} +1 -1
  126. rasa/core/channels/inspector/dist/assets/clone-26177ddb.js +1 -0
  127. rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-ca47fd38.js → createText-2e5e7dd3-c111213b.js} +1 -1
  128. rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-f837ca8a.js → edges-e0da2a9e-812a729d.js} +1 -1
  129. rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-8717ac54.js → erDiagram-9861fffd-fd5051bc.js} +1 -1
  130. rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-94f38b83.js → flowDb-956e92f1-3287ac02.js} +1 -1
  131. rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-b616f9fb.js → flowDiagram-66a62f08-692fb0b2.js} +1 -1
  132. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-29c03f5a.js +1 -0
  133. rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-f5d24bb8.js → flowchart-elk-definition-4a651766-008376f1.js} +1 -1
  134. rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-b43ba8d9.js → ganttDiagram-c361ad54-df330a69.js} +1 -1
  135. rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-c3aafaa5.js → gitGraphDiagram-72cf32ee-e03676fb.js} +1 -1
  136. rasa/core/channels/inspector/dist/assets/{graph-0d0a2c10.js → graph-46fad2ba.js} +1 -1
  137. rasa/core/channels/inspector/dist/assets/{index-3862675e-58ea0305.js → index-3862675e-a484ac55.js} +1 -1
  138. rasa/core/channels/inspector/dist/assets/{index-cce6f8a1.js → index-a003633f.js} +179 -179
  139. rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-b8f60461.js → infoDiagram-f8f76790-3f9e6ec2.js} +1 -1
  140. rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-95be5545.js → journeyDiagram-49397b02-79f72383.js} +1 -1
  141. rasa/core/channels/inspector/dist/assets/{layout-da885b9b.js → layout-aad098e5.js} +1 -1
  142. rasa/core/channels/inspector/dist/assets/{line-f1c817d3.js → line-219ab7ae.js} +1 -1
  143. rasa/core/channels/inspector/dist/assets/{linear-d42801e6.js → linear-2cddbe62.js} +1 -1
  144. rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-a38923a6.js → mindmap-definition-fc14e90a-1d41ed99.js} +1 -1
  145. rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-ca6e71e9.js → pieDiagram-8a3498a8-cc496ee8.js} +1 -1
  146. rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-b290dae9.js → quadrantDiagram-120e2f19-84d32884.js} +1 -1
  147. rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-03f02ceb.js → requirementDiagram-deff3bca-c0deb984.js} +1 -1
  148. rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-c49eee40.js → sankeyDiagram-04a897e0-b9d7fd62.js} +1 -1
  149. rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-b2cd6a3d.js → sequenceDiagram-704730f1-7d517565.js} +1 -1
  150. rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-e53a2028.js → stateDiagram-587899a1-98ef9b27.js} +1 -1
  151. rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-e1982a03.js → stateDiagram-v2-d93cdb3a-cee70748.js} +1 -1
  152. rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-d0226ca5.js → styles-6aaf32cf-3f9d1c96.js} +1 -1
  153. rasa/core/channels/inspector/dist/assets/{styles-9a916d00-0e21dc00.js → styles-9a916d00-67471923.js} +1 -1
  154. rasa/core/channels/inspector/dist/assets/{styles-c10674c1-9588494e.js → styles-c10674c1-bd093fb7.js} +1 -1
  155. rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-be478d4f.js → svgDrawCommon-08f97a94-675794e8.js} +1 -1
  156. rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-74631749.js → timeline-definition-85554ec2-0ac67617.js} +1 -1
  157. rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-a043552f.js → xychartDiagram-e933f94c-c018dc37.js} +1 -1
  158. rasa/core/channels/inspector/dist/index.html +2 -2
  159. rasa/core/channels/inspector/index.html +1 -1
  160. rasa/core/channels/inspector/package.json +4 -3
  161. rasa/core/channels/inspector/src/App.tsx +53 -7
  162. rasa/core/channels/inspector/src/components/Chat.tsx +3 -2
  163. rasa/core/channels/inspector/src/components/DiagramFlow.tsx +1 -1
  164. rasa/core/channels/inspector/src/components/LatencyDisplay.tsx +268 -0
  165. rasa/core/channels/inspector/src/components/LoadingSpinner.tsx +6 -2
  166. rasa/core/channels/inspector/src/helpers/audio/audiostream.ts +8 -3
  167. rasa/core/channels/inspector/src/types.ts +8 -0
  168. rasa/core/channels/inspector/yarn.lock +12 -12
  169. rasa/core/channels/studio_chat.py +119 -34
  170. rasa/core/channels/voice_ready/twilio_voice.py +1 -1
  171. rasa/core/channels/voice_stream/asr/asr_engine.py +5 -1
  172. rasa/core/channels/voice_stream/asr/deepgram.py +5 -0
  173. rasa/core/channels/voice_stream/audiocodes.py +16 -8
  174. rasa/core/channels/voice_stream/browser_audio.py +39 -4
  175. rasa/core/channels/voice_stream/call_state.py +13 -2
  176. rasa/core/channels/voice_stream/genesys.py +16 -13
  177. rasa/core/channels/voice_stream/jambonz.py +14 -12
  178. rasa/core/channels/voice_stream/twilio_media_streams.py +14 -13
  179. rasa/core/channels/voice_stream/util.py +11 -1
  180. rasa/core/channels/voice_stream/voice_channel.py +108 -29
  181. rasa/core/nlg/callback.py +1 -1
  182. rasa/core/nlg/contextual_response_rephraser.py +19 -9
  183. rasa/core/nlg/generator.py +21 -5
  184. rasa/core/nlg/response.py +43 -6
  185. rasa/core/nlg/translate.py +8 -0
  186. rasa/core/policies/enterprise_search_policy.py +16 -21
  187. rasa/dialogue_understanding/commands/correct_slots_command.py +38 -10
  188. rasa/dialogue_understanding/generator/command_generator.py +5 -5
  189. rasa/dialogue_understanding/generator/command_parser.py +9 -13
  190. rasa/dialogue_understanding/processor/command_processor.py +149 -55
  191. rasa/dialogue_understanding/stack/utils.py +13 -3
  192. rasa/dialogue_understanding_test/du_test_schema.yml +3 -3
  193. rasa/dialogue_understanding_test/validation.py +9 -10
  194. rasa/e2e_test/e2e_config.py +18 -11
  195. rasa/e2e_test/e2e_test_schema.yml +3 -3
  196. rasa/e2e_test/utils/validation.py +17 -19
  197. rasa/engine/validation.py +86 -91
  198. rasa/exceptions.py +26 -1
  199. rasa/model_manager/model_api.py +2 -2
  200. rasa/model_manager/socket_bridge.py +8 -2
  201. rasa/shared/providers/_configs/default_litellm_client_config.py +3 -7
  202. rasa/shared/utils/cli.py +2 -0
  203. rasa/shared/utils/common.py +2 -1
  204. rasa/shared/utils/health_check/health_check.py +10 -14
  205. rasa/studio/upload.py +6 -2
  206. rasa/studio/utils.py +33 -22
  207. rasa/telemetry.py +95 -22
  208. rasa/utils/licensing.py +21 -10
  209. rasa/utils/log_utils.py +1 -1
  210. rasa/utils/tensorflow/transformer.py +3 -3
  211. rasa/validator.py +7 -5
  212. rasa/version.py +1 -1
  213. {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/METADATA +7 -7
  214. {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/RECORD +242 -203
  215. rasa/builder/create_openai_vector_store.py +0 -228
  216. rasa/builder/llm-helper-schema.json +0 -69
  217. rasa/builder/llm_context.py +0 -81
  218. rasa/builder/llm_helper_prompt.jinja2 +0 -245
  219. rasa/cli/project_templates/finance/data/nlu.yml +0 -29
  220. rasa/cli/project_templates/finance/data/patterns/pattern_search.yml +0 -5
  221. rasa/cli/project_templates/finance/domain/default_flows.yml +0 -33
  222. rasa/cli/project_templates/finance/prompts/command-generator.jinja2 +0 -57
  223. rasa/cli/project_templates/finance/tests/conversation_repair/cancellations.yml +0 -12
  224. rasa/cli/project_templates/finance/tests/conversation_repair/cannot_handle.yml +0 -7
  225. rasa/cli/project_templates/finance/tests/conversation_repair/chitchat.yml +0 -7
  226. rasa/cli/project_templates/finance/tests/conversation_repair/clarification.yml +0 -9
  227. rasa/cli/project_templates/finance/tests/conversation_repair/completion.yml +0 -18
  228. rasa/cli/project_templates/finance/tests/conversation_repair/corrections.yml +0 -17
  229. rasa/cli/project_templates/finance/tests/conversation_repair/digressions.yml +0 -32
  230. rasa/cli/project_templates/finance/tests/conversation_repair/human_handoff.yml +0 -21
  231. rasa/cli/project_templates/finance/tests/conversation_repair/skipping_collect_steps.yml +0 -16
  232. rasa/cli/project_templates/finance/tests/demo_scripts/main.yml +0 -16
  233. rasa/cli/project_templates/finance/tests/happy_paths/balance_verification.yml +0 -15
  234. rasa/cli/project_templates/finance/tests/happy_paths/banking_questions.yml +0 -12
  235. rasa/cli/project_templates/finance/tests/happy_paths/card_blocking.yml +0 -52
  236. rasa/cli/project_templates/finance/tests/happy_paths/money_transfer.yml +0 -136
  237. rasa/cli/project_templates/finance/tests/happy_paths/payee_management.yml +0 -27
  238. rasa/cli/project_templates/finance/tests/happy_paths/user_greeted.yml +0 -5
  239. rasa/cli/project_templates/plain/config.yml +0 -17
  240. rasa/cli/project_templates/plain/data/patterns/pattern_session_start.yml +0 -7
  241. rasa/cli/project_templates/plain/domain.yml +0 -5
  242. rasa/core/channels/inspector/dist/assets/channel-f1efda17.js +0 -1
  243. rasa/core/channels/inspector/dist/assets/clone-fdf164e2.js +0 -1
  244. rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-7d7a1629.js +0 -1
  245. rasa/shared/importers/static.py +0 -63
  246. /rasa/{cli/project_templates/plain/actions → builder/copilot}/__init__.py +0 -0
  247. /rasa/builder/{inkeep-rag-response-schema.json → document_retrieval/inkeep-rag-response-schema.json} +0 -0
  248. /rasa/cli/project_templates/finance/actions/{action_process_immediate_payment.py → transfers/action_process_immediate_payment.py} +0 -0
  249. /rasa/cli/project_templates/finance/actions/{action_schedule_payment.py → transfers/action_schedule_payment.py} +0 -0
  250. /rasa/cli/project_templates/finance/actions/{action_validate_payment_date.py → transfers/action_validate_payment_date.py} +0 -0
  251. /rasa/cli/project_templates/finance/data/{flows → cards}/block_card.yml +0 -0
  252. /rasa/cli/project_templates/finance/data/{flows → cards}/select_card.yml +0 -0
  253. /rasa/cli/project_templates/finance/data/{source → system/source}/accounts.json +0 -0
  254. /rasa/cli/project_templates/finance/data/{source → system/source}/advisors.json +0 -0
  255. /rasa/cli/project_templates/finance/data/{source → system/source}/appointments.json +0 -0
  256. /rasa/cli/project_templates/finance/data/{source → system/source}/branches.json +0 -0
  257. /rasa/cli/project_templates/finance/data/{source → system/source}/cards.json +0 -0
  258. /rasa/cli/project_templates/finance/data/{source → system/source}/payees.json +0 -0
  259. /rasa/cli/project_templates/finance/data/{source → system/source}/transactions.json +0 -0
  260. /rasa/cli/project_templates/finance/data/{source → system/source}/users.json +0 -0
  261. /rasa/cli/project_templates/finance/data/{flows → transfers}/add_payee.yml +0 -0
  262. /rasa/cli/project_templates/finance/data/{flows → transfers}/list_payees.yml +0 -0
  263. /rasa/cli/project_templates/finance/data/{flows → transfers}/remove_payee.yml +0 -0
  264. /rasa/cli/project_templates/finance/data/{flows → transfers}/transfer_money.yml +0 -0
  265. /rasa/cli/project_templates/finance/domain/{block_card.yml → cards/block_card.yml} +0 -0
  266. /rasa/cli/project_templates/finance/domain/{select_card.yml → cards/select_card.yml} +0 -0
  267. /rasa/cli/project_templates/finance/domain/{add_payee.yml → transfers/add_payee.yml} +0 -0
  268. /rasa/cli/project_templates/finance/domain/{list_payees.yml → transfers/list_payees.yml} +0 -0
  269. /rasa/cli/project_templates/finance/domain/{remove_payee.yml → transfers/remove_payee.yml} +0 -0
  270. /rasa/cli/project_templates/finance/domain/{transfer_money.yml → transfers/transfer_money.yml} +0 -0
  271. {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/NOTICE +0 -0
  272. {rasa_pro-3.13.1a18.dist-info → rasa_pro-3.13.1a20.dist-info}/WHEEL +0 -0
  273. {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
- """Main service for the prompt-to-bot functionality."""
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.exceptions import (
12
- LLMGenerationError,
13
- ProjectGenerationError,
14
- TrainingError,
15
- ValidationError,
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 get_recent_logs
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
- LLMBuilderContext,
22
- LLMBuilderRequest,
23
- LLMHelperResponse,
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.training_service import train_and_load_agent
30
- from rasa.builder.validation_service import validate_project
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.studio.upload import CALMUserData, extract_calm_import_parts_from_importer
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 extract_calm_import_parts_from_project_generator(
72
- project_generator: ProjectGenerator,
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
- project_generator: The project generator to extract data from
113
+ agent: The agent to extract data from
78
114
 
79
115
  Returns:
80
- CALMUserData containing flows, domain, config, endpoints, and nlu data
116
+ BotData containing flows, domain, config, endpoints, and nlu data
81
117
  """
82
- # Get the training data importer
83
- importer = project_generator._create_importer()
84
-
85
- # Extract endpoints (if exists)
86
- endpoints_path = project_generator.project_folder / "endpoints.yml"
87
- if endpoints_path.exists():
88
- from rasa.shared.utils.yaml import read_yaml_file
89
-
90
- endpoints = read_yaml_file(endpoints_path, expand_env_vars=False)
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
- "using LLM generation. Returns server-sent events (SSE) for real-time "
118
- "progress tracking through the entire bot creation process.\n\n"
119
- "**SSE Event Flow:**\n"
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 (can occur at any time):**\n"
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:** Send POST request with Content-Type: application/json and "
132
- "Accept: text/event-stream"
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 and client ID "
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
- {"text/event-stream": str},
144
- description="Server-sent events stream with real-time progress updates",
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) -> None:
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
- # 1. Received
164
- await _send_sse_event(
165
- sse_response,
166
- ServerSentEvent(event="received", data={"status": "received"}),
167
- )
168
-
169
- # Validate request
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
- except Exception as e:
266
- structlogger.error(
267
- "bot_builder_service.prompt_to_bot.unexpected_error", error=str(e)
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
- await _send_sse_event(
270
- sse_response,
271
- ServerSentEvent(event="error", data={"status": "error", "error": str(e)}),
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 with "
281
- "immediate setup. Returns server-sent events (SSE) for real-time progress "
282
- "tracking through the entire bot creation process.\n\n"
283
- "**SSE Event Flow:**\n"
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 (can occur at any time):**\n"
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:** Send POST request with Content-Type: application/json and "
296
- "Accept: text/event-stream\n"
297
- "**Templates Available:** Check available templates through the API or "
298
- "documentation"
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 and client ID for " "tracking",
327
+ description="Template request with template name.",
304
328
  required=True,
305
329
  )
306
330
  @openapi.response(
307
331
  200,
308
- {"text/event-stream": model_to_schema(ServerSentEvent)},
309
- description="Server-sent events stream with real-time progress updates",
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) -> None:
326
- """Handle template-to-bot generation requests."""
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
- # 2. Generating
342
- await _send_sse_event(
343
- sse_response,
344
- ServerSentEvent(event="generating", data={"status": "generating"}),
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
- except ValidationError as e:
423
- structlogger.error(
424
- "bot_builder_service.template_to_bot.validation_error", error=str(e)
425
- )
426
- await _send_sse_event(
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 e:
435
- structlogger.error(
436
- "bot_builder_service.template_to_bot.unexpected_error", error=str(e)
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
- await _send_sse_event(
439
- sse_response,
440
- ServerSentEvent(event="error", data={"status": "error", "error": str(e)}),
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
- project_generator = get_project_generator(request)
463
- bot_files = project_generator.get_bot_files()
464
- return response.json(bot_files)
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 server-sent events (SSE) for real-time progress tracking "
472
- "through the entire update process.\n\n"
473
- "**SSE Event Flow:**\n"
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:** Send PUT request with Content-Type: application/json and "
485
- "Accept: text/event-stream"
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
- {"text/event-stream": str},
498
- description="Server-sent events stream with update progress",
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) -> None:
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
- project_generator.update_bot_files(bot_files)
526
-
527
- # 2. Validating
528
- await _send_sse_event(
529
- sse_response,
530
- ServerSentEvent(event="validating", data={"status": "validating"}),
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
- try:
566
- request.app.ctx.agent = await train_and_load_agent(importer)
567
- input_channel.agent = request.app.ctx.agent
568
-
569
- await _send_sse_event(
570
- sse_response,
571
- ServerSentEvent(
572
- event="train_success", data={"status": "train_success"}
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
- except Exception as e:
599
- await _send_sse_event(
600
- sse_response,
601
- ServerSentEvent(event="error", data={"status": "error", "error": str(e)}),
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(CALMUserData)},
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
- project_generator = get_project_generator(request)
628
- calm_parts = extract_calm_import_parts_from_project_generator(project_generator)
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(calm_parts.model_dump())
631
- except Exception as e:
632
- structlogger.error("bot_builder_service.get_bot_data.error", error=str(e))
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("/llm-builder", methods=["POST"])
643
- @openapi.summary("LLM assistant for bot building")
711
+ @bp.route("/copilot", methods=["POST"])
712
+ @openapi.summary("AI copilot for bot building")
644
713
  @openapi.description(
645
- "Provides LLM-powered assistance for bot building tasks, including "
646
- "debugging, suggestions, and explanations"
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("llm-assistant")
719
+ @openapi.tag("copilot")
649
720
  @openapi.body(
650
- {"application/json": model_to_schema(LLMBuilderRequest)},
651
- description="LLM builder request containing chat messages and context",
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
- {"application/json": model_to_schema(LLMHelperResponse)},
657
- description="LLM response with assistance and suggestions",
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 llm_builder(request: Request) -> HTTPResponse:
675
- """Handle LLM builder requests."""
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 request
681
- builder_request = LLMBuilderRequest(**request.json)
815
+ # 1. Validate and unpack input
816
+ req = CopilotRequest(**request.json)
682
817
 
683
- # Get current conversation context
684
- current_tracker = await current_tracker_from_input_channel(
685
- request.app, input_channel
818
+ telemetry = CopilotTelemetry(
819
+ project_id=HELLO_RASA_PROJECT_ID,
820
+ user_id=request.headers.get(HEADER_USER_ID),
686
821
  )
687
- bot_logs = get_recent_logs()
688
- chat_bot_files = project_generator.get_bot_files()
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
- # create LLM builder context
691
- llm_builder_context = LLMBuilderContext(
692
- tracker=current_tracker,
693
- bot_logs=bot_logs,
694
- chat_bot_files=chat_bot_files,
695
- chat_history=builder_request.messages,
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
- # Generate response
699
- messages = await llm_service.create_helper_messages(llm_builder_context)
700
- llm_response = await llm_service.generate_helper_response(messages)
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
- return response.json(llm_response)
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
- except LLMGenerationError as e:
705
- structlogger.error(
706
- "bot_builder_service.llm_builder.generation_error", error=str(e)
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
- return response.json(
709
- ApiErrorResponse(
710
- error="LLM helper generation failed", details={"llm_error": str(e)}
711
- ).model_dump(),
712
- status=502,
908
+ await sse.send(
909
+ ServerSentEvent(
910
+ event="error",
911
+ data={"error": str(e)},
912
+ ).format()
713
913
  )
714
914
 
715
- except Exception as e:
716
- structlogger.error(
717
- "bot_builder_service.llm_builder.unexpected_error", error=str(e)
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
- return response.json(
720
- ApiErrorResponse(
721
- error="Unexpected error in LLM builder",
722
- details=None,
723
- ).model_dump(),
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, input_channel: StudioChatInput
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 input_channel.latest_tracker_session_id:
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())