rasa-pro 3.14.0rc3__py3-none-any.whl → 3.14.1__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 (55) hide show
  1. rasa/agents/agent_manager.py +7 -5
  2. rasa/agents/protocol/a2a/a2a_agent.py +13 -11
  3. rasa/agents/protocol/mcp/mcp_base_agent.py +64 -12
  4. rasa/agents/validation.py +61 -6
  5. rasa/builder/copilot/constants.py +4 -0
  6. rasa/builder/copilot/copilot_templated_message_provider.py +24 -1
  7. rasa/builder/copilot/templated_messages/copilot_templated_responses.yml +3 -0
  8. rasa/builder/copilot/templated_messages/copilot_welcome_messages.yml +56 -0
  9. rasa/builder/jobs.py +162 -5
  10. rasa/builder/models.py +3 -0
  11. rasa/builder/validation_service.py +4 -0
  12. rasa/cli/arguments/data.py +9 -0
  13. rasa/cli/data.py +72 -6
  14. rasa/cli/interactive.py +3 -0
  15. rasa/cli/llm_fine_tuning.py +1 -0
  16. rasa/cli/project_templates/defaults.py +1 -0
  17. rasa/cli/validation/bot_config.py +4 -0
  18. rasa/constants.py +1 -1
  19. rasa/core/actions/action_exceptions.py +1 -1
  20. rasa/core/agent.py +4 -1
  21. rasa/core/available_agents.py +1 -1
  22. rasa/core/config/available_endpoints.py +6 -0
  23. rasa/core/exceptions.py +1 -1
  24. rasa/core/featurizers/tracker_featurizers.py +3 -2
  25. rasa/core/persistor.py +7 -7
  26. rasa/core/policies/flows/agent_executor.py +84 -4
  27. rasa/core/policies/flows/flow_exceptions.py +5 -2
  28. rasa/core/policies/flows/flow_executor.py +3 -2
  29. rasa/core/policies/flows/mcp_tool_executor.py +7 -1
  30. rasa/core/policies/rule_policy.py +1 -1
  31. rasa/dialogue_understanding/commands/cancel_flow_command.py +1 -1
  32. rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +22 -20
  33. rasa/engine/validation.py +6 -2
  34. rasa/model_manager/runner_service.py +1 -1
  35. rasa/privacy/privacy_config.py +1 -1
  36. rasa/shared/agents/auth/auth_strategy/oauth2_auth_strategy.py +4 -7
  37. rasa/shared/agents/auth/utils.py +85 -0
  38. rasa/shared/constants.py +3 -0
  39. rasa/shared/core/training_data/story_reader/story_reader.py +1 -1
  40. rasa/shared/exceptions.py +23 -2
  41. rasa/shared/providers/llm/litellm_router_llm_client.py +2 -2
  42. rasa/shared/utils/llm.py +54 -4
  43. rasa/shared/utils/mcp/server_connection.py +7 -4
  44. rasa/studio/download.py +3 -0
  45. rasa/studio/prompts.py +1 -0
  46. rasa/studio/upload.py +4 -0
  47. rasa/utils/log_utils.py +20 -1
  48. rasa/utils/pypred.py +7 -0
  49. rasa/validator.py +90 -2
  50. rasa/version.py +1 -1
  51. {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/METADATA +8 -7
  52. {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/RECORD +55 -53
  53. {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/NOTICE +0 -0
  54. {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/WHEEL +0 -0
  55. {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- from typing import Any, Dict, List, Optional, cast
4
+ from typing import Any, Dict, List, Optional, Tuple, cast
5
5
 
6
6
  import structlog
7
7
 
@@ -24,6 +24,7 @@ from rasa.core.policies.flows.flow_step_result import (
24
24
  PauseFlowReturnPrediction,
25
25
  )
26
26
  from rasa.core.utils import get_slot_names_from_exit_conditions
27
+ from rasa.dialogue_understanding.patterns.cancel import CancelPatternFlowStackFrame
27
28
  from rasa.dialogue_understanding.patterns.internal_error import (
28
29
  InternalErrorPatternFlowStackFrame,
29
30
  )
@@ -31,6 +32,7 @@ from rasa.dialogue_understanding.stack.dialogue_stack import DialogueStack
31
32
  from rasa.dialogue_understanding.stack.frames.flow_stack_frame import (
32
33
  AgentStackFrame,
33
34
  AgentState,
35
+ BaseFlowStackFrame,
34
36
  )
35
37
  from rasa.shared.agents.utils import get_protocol_type
36
38
  from rasa.shared.core.constants import (
@@ -46,9 +48,11 @@ from rasa.shared.core.events import (
46
48
  AgentResumed,
47
49
  AgentStarted,
48
50
  Event,
51
+ FlowCancelled,
49
52
  SlotSet,
50
53
  deserialise_events,
51
54
  )
55
+ from rasa.shared.core.flows.flows_list import FlowsList
52
56
  from rasa.shared.core.flows.steps import (
53
57
  CallFlowStep,
54
58
  )
@@ -85,6 +89,7 @@ async def run_agent(
85
89
  step: CallFlowStep,
86
90
  tracker: DialogueStateTracker,
87
91
  slots: List[Slot],
92
+ flows: FlowsList,
88
93
  ) -> FlowStepResult:
89
94
  """Run an agent call step."""
90
95
  structlogger.debug(
@@ -177,9 +182,13 @@ async def run_agent(
177
182
  elif output.status == AgentStatus.COMPLETED:
178
183
  return _handle_agent_completed(output, final_events, stack, step)
179
184
  elif output.status == AgentStatus.FATAL_ERROR:
180
- return _handle_agent_fatal_error(output, final_events, stack, step)
185
+ return _handle_agent_fatal_error(
186
+ output, final_events, stack, step, flows, tracker
187
+ )
181
188
  else:
182
- return _handle_agent_unknown_status(output, final_events, stack, step)
189
+ return _handle_agent_unknown_status(
190
+ output, final_events, stack, step, flows, tracker
191
+ )
183
192
 
184
193
 
185
194
  async def _call_agent_with_retry(
@@ -299,6 +308,8 @@ def _handle_agent_unknown_status(
299
308
  final_events: List[Event],
300
309
  stack: DialogueStack,
301
310
  step: CallFlowStep,
311
+ flows: FlowsList,
312
+ tracker: DialogueStateTracker,
302
313
  ) -> FlowStepResult:
303
314
  """Handle unknown agent status.
304
315
 
@@ -307,6 +318,8 @@ def _handle_agent_unknown_status(
307
318
  final_events: List of events to be added to the final result
308
319
  stack: The dialogue stack
309
320
  step: The flow step that called the agent
321
+ flows: All flows
322
+ tracker: The dialogue state tracker
310
323
 
311
324
  Returns:
312
325
  FlowStepResult indicating to continue with internal error pattern
@@ -320,8 +333,21 @@ def _handle_agent_unknown_status(
320
333
  flow_id=step.flow_id,
321
334
  status=output.status,
322
335
  )
336
+ # remove the agent stack frame
323
337
  remove_agent_stack_frame(stack, step.call)
324
338
  final_events.append(AgentCancelled(agent_id=step.call, flow_id=step.flow_id))
339
+
340
+ # cancel the current active flow:
341
+ # push the cancel pattern stack frame and add the flow cancelled event
342
+ cancel_pattern_stack_frame, flow_cancelled_event = _cancel_flow(
343
+ stack, flows, tracker, step
344
+ )
345
+ if cancel_pattern_stack_frame:
346
+ stack.push(cancel_pattern_stack_frame)
347
+ if flow_cancelled_event:
348
+ final_events.append(flow_cancelled_event)
349
+
350
+ # trigger the internal error pattern
325
351
  stack.push(InternalErrorPatternFlowStackFrame())
326
352
  return ContinueFlowWithNextStep(events=final_events)
327
353
 
@@ -418,6 +444,8 @@ def _handle_agent_fatal_error(
418
444
  final_events: List[Event],
419
445
  stack: DialogueStack,
420
446
  step: CallFlowStep,
447
+ flows: FlowsList,
448
+ tracker: DialogueStateTracker,
421
449
  ) -> FlowStepResult:
422
450
  """Handle fatal error from agent execution.
423
451
 
@@ -426,13 +454,15 @@ def _handle_agent_fatal_error(
426
454
  final_events: List of events to be added to the final result
427
455
  stack: The dialogue stack
428
456
  step: The flow step that called the agent
457
+ flows: All flows
458
+ tracker: The dialogue state tracker
429
459
 
430
460
  Returns:
431
461
  FlowStepResult indicating to continue with internal error pattern
432
462
  """
433
463
  output.metadata = output.metadata or {}
434
464
  _update_agent_events(final_events, output.metadata)
435
- # the agent failed, trigger pattern_internal_error
465
+ # the agent failed, cancel the current flow and trigger pattern_internal_error
436
466
  structlogger.error(
437
467
  "flow.step.run_agent.fatal_error",
438
468
  agent_name=step.call,
@@ -440,16 +470,66 @@ def _handle_agent_fatal_error(
440
470
  flow_id=step.flow_id,
441
471
  error_message=output.error_message,
442
472
  )
473
+ # remove the agent stack frame
443
474
  remove_agent_stack_frame(stack, step.call)
444
475
  final_events.append(
445
476
  AgentCancelled(
446
477
  agent_id=step.call, flow_id=step.flow_id, reason=output.error_message
447
478
  )
448
479
  )
480
+
481
+ # cancel the current active flow:
482
+ # push the cancel pattern stack frame and add the flow cancelled event
483
+ cancel_pattern_stack_frame, flow_cancelled_event = _cancel_flow(
484
+ stack, flows, tracker, step
485
+ )
486
+ if cancel_pattern_stack_frame:
487
+ stack.push(cancel_pattern_stack_frame)
488
+ if flow_cancelled_event:
489
+ final_events.append(flow_cancelled_event)
490
+
491
+ # push the internal error pattern stack frame
449
492
  stack.push(InternalErrorPatternFlowStackFrame())
450
493
  return ContinueFlowWithNextStep(events=final_events)
451
494
 
452
495
 
496
+ def _cancel_flow(
497
+ stack: DialogueStack,
498
+ flows: FlowsList,
499
+ tracker: DialogueStateTracker,
500
+ step: CallFlowStep,
501
+ ) -> Tuple[Optional[CancelPatternFlowStackFrame], Optional[FlowCancelled]]:
502
+ """Cancel the current active flow.
503
+
504
+ Creates a cancel pattern stack frame and a flow cancelled event.
505
+ """
506
+ from rasa.dialogue_understanding.commands import CancelFlowCommand
507
+
508
+ cancel_pattern_stack_frame = None
509
+ flow_cancelled_event = None
510
+
511
+ top_frame = stack.top()
512
+
513
+ if isinstance(top_frame, BaseFlowStackFrame):
514
+ flow = flows.flow_by_id(step.flow_id)
515
+ flow_name = (
516
+ flow.readable_name(language=tracker.current_language)
517
+ if flow
518
+ else step.flow_id
519
+ )
520
+
521
+ canceled_frames = CancelFlowCommand.select_canceled_frames(stack)
522
+
523
+ cancel_pattern_stack_frame = CancelPatternFlowStackFrame(
524
+ canceled_name=flow_name,
525
+ canceled_frames=canceled_frames,
526
+ )
527
+
528
+ flow_cancelled_event = FlowCancelled(step.flow_id, step.id)
529
+
530
+ return cancel_pattern_stack_frame, flow_cancelled_event
531
+
532
+
453
533
  ################################################################################
454
534
  # Create predictions
455
535
  ################################################################################
@@ -5,14 +5,17 @@ from rasa.shared.exceptions import RasaException
5
5
  class FlowException(RasaException):
6
6
  """Exception that is raised when there is a problem with a flow."""
7
7
 
8
- pass
8
+ def __init__(self, message: str = "Flow error occurred") -> None:
9
+ """Initialize FlowException with a message."""
10
+ super().__init__(message)
9
11
 
10
12
 
11
13
  class FlowCircuitBreakerTrippedException(FlowException):
12
14
  """Exception that is raised when the flow circuit breaker tripped.
13
15
 
14
16
  The circuit breaker gets tripped when a flow seems to be stuck in
15
- executing steps and does not make any progress."""
17
+ executing steps and does not make any progress.
18
+ """
16
19
 
17
20
  def __init__(
18
21
  self, dialogue_stack: DialogueStack, number_of_steps_taken: int
@@ -650,7 +650,7 @@ async def run_step(
650
650
  return _run_link_step(initial_events, stack, step)
651
651
 
652
652
  elif isinstance(step, CallFlowStep):
653
- return await _run_call_step(initial_events, stack, step, tracker, slots)
653
+ return await _run_call_step(initial_events, stack, step, tracker, slots, flows)
654
654
 
655
655
  elif isinstance(step, SetSlotsFlowStep):
656
656
  return _run_set_slot_step(initial_events, step)
@@ -723,12 +723,13 @@ async def _run_call_step(
723
723
  step: CallFlowStep,
724
724
  tracker: DialogueStateTracker,
725
725
  slots: List[Slot],
726
+ flows: FlowsList,
726
727
  ) -> FlowStepResult:
727
728
  structlogger.debug("flow.step.run.call")
728
729
  if step.is_calling_mcp_tool():
729
730
  return await call_mcp_tool(initial_events, stack, step, tracker)
730
731
  elif step.is_calling_agent():
731
- return await run_agent(initial_events, stack, step, tracker, slots)
732
+ return await run_agent(initial_events, stack, step, tracker, slots, flows)
732
733
  else:
733
734
  stack.push(
734
735
  UserFlowStackFrame(
@@ -1,4 +1,5 @@
1
1
  import json
2
+ from datetime import timedelta
2
3
  from typing import Any, Dict, List, Optional
3
4
 
4
5
  import structlog
@@ -24,6 +25,7 @@ structlogger = structlog.get_logger()
24
25
 
25
26
  CONFIG_VALUE = "value"
26
27
  CONFIG_SLOT = "slot"
28
+ TOOL_CALL_DEFATULT_TIMEOUT = 10 # seconds
27
29
 
28
30
 
29
31
  async def call_mcp_tool(
@@ -102,7 +104,11 @@ async def _execute_mcp_tool_call(
102
104
 
103
105
  # Call the tool with parameters
104
106
  mcp_server = await mcp_server_connection.ensure_active_session()
105
- result = await mcp_server.call_tool(step.call, arguments)
107
+ result = await mcp_server.call_tool(
108
+ step.call,
109
+ arguments,
110
+ read_timeout_seconds=timedelta(seconds=TOOL_CALL_DEFATULT_TIMEOUT),
111
+ )
106
112
 
107
113
  # Handle tool execution result
108
114
  if result is None or result.isError:
@@ -84,7 +84,7 @@ class InvalidRule(RasaException):
84
84
  """Exception that can be raised when rules are not valid."""
85
85
 
86
86
  def __init__(self, message: Text) -> None:
87
- super().__init__()
87
+ super().__init__(message)
88
88
  self.message = message
89
89
 
90
90
  def __str__(self) -> Text:
@@ -56,7 +56,7 @@ class CancelFlowCommand(Command):
56
56
  Returns:
57
57
  The frames that were canceled.
58
58
  """
59
- canceled_frames = []
59
+ canceled_frames: List[str] = []
60
60
  # we need to go through the original stack dump in reverse order
61
61
  # to find the frames that were canceled. we cancel everything from
62
62
  # the top of the stack until we hit the user flow that was canceled.
@@ -227,28 +227,30 @@ flows:
227
227
  - noop: true
228
228
  next:
229
229
  - if: context.multiple_flows_interrupted
230
+ then: collect_interrupted_flow_to_continue
231
+ - else: collect_continue_interrupted_flow_confirmation
232
+ - id: collect_interrupted_flow_to_continue
233
+ collect: interrupted_flow_to_continue
234
+ description: "Fill this slot with the name of the flow the user wants to continue. If the user does not want to continue any of the interrupted flows, fill this slot with 'none'."
235
+ next:
236
+ - if: slots.interrupted_flow_to_continue is not "none"
237
+ then:
238
+ - action: action_continue_interrupted_flow
239
+ next: END
240
+ - else:
241
+ - action: action_cancel_interrupted_flows
242
+ next: END
243
+ - id: collect_continue_interrupted_flow_confirmation
244
+ collect: continue_interrupted_flow_confirmation
245
+ description: "If the user wants to continue the interrupted flow, fill this slot with true. If the user does not want to continue the interrupted flow, fill this slot with false."
246
+ next:
247
+ - if: slots.continue_interrupted_flow_confirmation
230
248
  then:
231
- - collect: interrupted_flow_to_continue
232
- description: "Fill this slot with the name of the flow the user wants to continue. If the user does not want to continue any of the interrupted flows, fill this slot with 'none'."
233
- next:
234
- - if: slots.interrupted_flow_to_continue != "none"
235
- then:
236
- - action: action_continue_interrupted_flow
237
- next: END
238
- - else:
239
- - action: action_cancel_interrupted_flows
240
- next: END
249
+ - action: action_continue_interrupted_flow
250
+ next: END
241
251
  - else:
242
- - collect: continue_interrupted_flow_confirmation
243
- description: "If the user wants to continue the interrupted flow, fill this slot with true. If the user does not want to continue the interrupted flow, fill this slot with false."
244
- next:
245
- - if: slots.continue_interrupted_flow_confirmation
246
- then:
247
- - action: action_continue_interrupted_flow
248
- next: END
249
- - else:
250
- - action: action_cancel_interrupted_flows
251
- next: END
252
+ - action: action_cancel_interrupted_flows
253
+ next: END
252
254
 
253
255
  pattern_correction:
254
256
  description: Conversation repair flow for managing user input changes or error corrections
rasa/engine/validation.py CHANGED
@@ -81,6 +81,7 @@ from rasa.shared.constants import (
81
81
  ROUTING_STRATEGIES_NOT_REQUIRING_CACHE,
82
82
  ROUTING_STRATEGIES_REQUIRING_REDIS_CACHE,
83
83
  ROUTING_STRATEGY_CONFIG_KEY,
84
+ SECRET_DATA_FORMAT_PATTERN,
84
85
  SENSITIVE_DATA,
85
86
  USE_CHAT_COMPLETIONS_ENDPOINT_CONFIG_KEY,
86
87
  VALID_PROVIDERS_FOR_API_TYPE_CONFIG_KEY,
@@ -1359,7 +1360,10 @@ def _validate_usage_of_environment_variables_in_model_group_config(
1359
1360
  for model_config in model_group[MODELS_CONFIG_KEY]:
1360
1361
  for key, value in model_config.items():
1361
1362
  if isinstance(value, str):
1362
- if re.match(r"\${(\w+)}", value) and key not in allowed_env_vars:
1363
+ if (
1364
+ re.match(SECRET_DATA_FORMAT_PATTERN, value)
1365
+ and key not in allowed_env_vars
1366
+ ):
1363
1367
  raise ValidationError(
1364
1368
  code="engine.validation.validate_model_group_configuration_setup"
1365
1369
  ".invalid_use_of_environment_variables",
@@ -1386,7 +1390,7 @@ def _validate_sensitive_keys_are_an_environment_variables_for_model_groups(
1386
1390
  for key, value in model_config.items():
1387
1391
  if key in SENSITIVE_DATA:
1388
1392
  if isinstance(value, str):
1389
- if not re.match(r"\${(\w+)}", value):
1393
+ if not re.match(SECRET_DATA_FORMAT_PATTERN, value):
1390
1394
  raise ValidationError(
1391
1395
  code="engine.validation.validate_model_group_configuration_setup"
1392
1396
  ".sensitive_key_string_value_must_be_set_as_env_var",
@@ -195,7 +195,7 @@ def fetch_remote_model_to_dir(
195
195
  try:
196
196
  return persistor.retrieve(model_name=model_name, target_path=target_path)
197
197
  except FileNotFoundError as e:
198
- raise ModelNotFound() from e
198
+ raise ModelNotFound("Model not found") from e
199
199
 
200
200
 
201
201
  def fetch_size_of_remote_model(
@@ -211,7 +211,7 @@ def get_cron_trigger(cron_expression: str) -> CronTrigger:
211
211
  "privacy_config.invalid_cron_expression",
212
212
  cron=cron_expression,
213
213
  )
214
- raise RasaException from exc
214
+ raise RasaException("Invalid cron expression") from exc
215
215
 
216
216
  return cron
217
217
 
@@ -139,20 +139,17 @@ class OAuth2AuthStrategy(AgentAuthStrategy):
139
139
  resp.raise_for_status()
140
140
  token_data = resp.json()
141
141
  except httpx.HTTPStatusError as e:
142
- raise ValueError(
143
- f"OAuth2 token request failed with status {e.response.status_code}: "
144
- f"{e.response.text}"
145
- ) from e
142
+ raise e
146
143
  except httpx.RequestError as e:
147
- raise ValueError(f"OAuth2 token request failed: {e}") from e
144
+ raise ValueError(f"OAuth2 token request failed - {e}") from e
148
145
  except Exception as e:
149
146
  raise ValueError(
150
- f"Unexpected error during OAuth2 token request: {e}"
147
+ f"Unexpected error during OAuth2 token request - {e}"
151
148
  ) from e
152
149
 
153
150
  # Validate token data
154
151
  if KEY_ACCESS_TOKEN not in token_data:
155
- raise ValueError(f"No {KEY_ACCESS_TOKEN} in OAuth2 response")
152
+ raise ValueError(f"No `{KEY_ACCESS_TOKEN}` in OAuth2 response")
156
153
 
157
154
  # Set access token and expires at
158
155
  self._access_token = token_data[KEY_ACCESS_TOKEN]
@@ -0,0 +1,85 @@
1
+ """Utilities for agent authentication and secret validation."""
2
+
3
+ import re
4
+ from typing import Any, Dict
5
+
6
+ from rasa.exceptions import ValidationError
7
+ from rasa.shared.agents.auth.constants import (
8
+ CONFIG_API_KEY_KEY,
9
+ CONFIG_CLIENT_SECRET_KEY,
10
+ CONFIG_OAUTH_KEY,
11
+ CONFIG_TOKEN_KEY,
12
+ )
13
+ from rasa.shared.constants import SECRET_DATA_FORMAT_PATTERN
14
+
15
+ AUTH_SECRETS = [CONFIG_API_KEY_KEY, CONFIG_TOKEN_KEY, CONFIG_CLIENT_SECRET_KEY]
16
+
17
+
18
+ def _is_valid_secret_data_format(value: str) -> bool:
19
+ """Check if a value is in the correct environment variable format.
20
+
21
+ Args:
22
+ value: The value to check
23
+
24
+ Returns:
25
+ True if the value is in format "${env_var}", False otherwise
26
+ """
27
+ if not isinstance(value, str):
28
+ return False
29
+
30
+ # Use the common regex pattern for environment variable validation
31
+ return bool(re.match(SECRET_DATA_FORMAT_PATTERN, value))
32
+
33
+
34
+ def _validate_secret_value(value: Any, key: str, context: str) -> None:
35
+ """Generic function to validate a single secret value.
36
+
37
+ Args:
38
+ value: The value to validate
39
+ key: The key name for error messages
40
+ context: Context for error messages
41
+ (e.g., "agent 'my_agent'", "MCP server 'my_server'")
42
+ """
43
+ if isinstance(value, str):
44
+ if not _is_valid_secret_data_format(value):
45
+ raise ValidationError(
46
+ code="validation.sensitive_key_string_value_must_be_set_as_env_var",
47
+ event_info=(
48
+ f"You defined the '{key}' in {context} as a string. The '{key}' "
49
+ f"must be set as an environment variable. Please update your "
50
+ f"config."
51
+ ),
52
+ key=key,
53
+ )
54
+ else:
55
+ raise ValidationError(
56
+ code="validation.sensitive_key_must_be_set_as_env_var",
57
+ event_info=(
58
+ f"You should define the '{key}' in {context} using the environment "
59
+ f"variable syntax - ${{ENV_VARIABLE_NAME}}. Please update your config."
60
+ ),
61
+ key=key,
62
+ )
63
+
64
+
65
+ def validate_secrets_in_params(
66
+ params: Dict[str, Any], context_name: str = "configuration"
67
+ ) -> None:
68
+ """Validate that secrets in params are in environment variable format.
69
+
70
+ Args:
71
+ params: The parameters dictionary to validate
72
+ context_name: Name of the context for error messages
73
+ (e.g., "agent", "MCP server")
74
+ """
75
+ for key, value in params.items():
76
+ if key in AUTH_SECRETS:
77
+ _validate_secret_value(value, key, context_name)
78
+ elif key == CONFIG_OAUTH_KEY and isinstance(value, dict):
79
+ # Handle oauth object specifically - we know it contains client_secret
80
+ if CONFIG_CLIENT_SECRET_KEY in value:
81
+ _validate_secret_value(
82
+ value[CONFIG_CLIENT_SECRET_KEY],
83
+ CONFIG_CLIENT_SECRET_KEY,
84
+ context_name,
85
+ )
rasa/shared/constants.py CHANGED
@@ -367,3 +367,6 @@ FAQ_DOCUMENT_LINE_SEPARATOR = "\n"
367
367
 
368
368
  # Constants for the MCP server
369
369
  KEY_TOOL_CALLS = "tool_calls"
370
+
371
+ # Regex pattern for secrets data validation
372
+ SECRET_DATA_FORMAT_PATTERN = r"\${(\w+)}"
@@ -126,4 +126,4 @@ class StoryParseError(RasaCoreException, ValueError):
126
126
 
127
127
  def __init__(self, message: Text) -> None:
128
128
  self.message = message
129
- super(StoryParseError, self).__init__()
129
+ super(StoryParseError, self).__init__(message)
rasa/shared/exceptions.py CHANGED
@@ -16,6 +16,17 @@ class RasaException(Exception):
16
16
  to the users, but will be ignored in telemetry.
17
17
  """
18
18
 
19
+ def __init__(self, message: str, suppress_stack_trace: bool = False, **kwargs: Any):
20
+ """Initialize the exception.
21
+
22
+ Args:
23
+ message: The error message.
24
+ suppress_stack_trace: If True, the stack trace will be suppressed in logs.
25
+ **kwargs: Additional keyword arguments (e.g., cause for exception chaining).
26
+ """
27
+ Exception.__init__(self, message)
28
+ self.suppress_stack_trace = suppress_stack_trace
29
+
19
30
 
20
31
  class RasaCoreException(RasaException):
21
32
  """Basic exception for errors raised by Rasa Core."""
@@ -113,6 +124,17 @@ class SchemaValidationError(RasaException, jsonschema.ValidationError):
113
124
  class InvalidEntityFormatException(RasaException, json.JSONDecodeError):
114
125
  """Raised if the format of an entity is invalid."""
115
126
 
127
+ def __init__(self, msg: str, doc: str = "", pos: int = 0):
128
+ """Initialize the exception.
129
+
130
+ Args:
131
+ msg: The error message.
132
+ doc: The document that caused the error.
133
+ pos: The position in the document where the error occurred.
134
+ """
135
+ RasaException.__init__(self, msg)
136
+ json.JSONDecodeError.__init__(self, msg, doc, pos)
137
+
116
138
  @classmethod
117
139
  def create_from(
118
140
  cls, other: json.JSONDecodeError, msg: Text
@@ -130,8 +152,7 @@ class ConnectionException(RasaException):
130
152
 
131
153
 
132
154
  class ProviderClientAPIException(RasaException):
133
- """Raised for errors that occur during API interactions
134
- with LLM / embedding providers.
155
+ """For errors during API interactions with LLM / embedding providers.
135
156
 
136
157
  Attributes:
137
158
  original_exception (Exception): The original exception that was
@@ -151,7 +151,7 @@ class LiteLLMRouterLLMClient(_BaseLiteLLMRouterClient, _BaseLiteLLMClient):
151
151
  if not self._use_chat_completions_endpoint:
152
152
  return self._text_completion(messages)
153
153
  try:
154
- formatted_messages = self._format_messages(messages)
154
+ formatted_messages = self._get_formatted_messages(messages)
155
155
  response = self.router_client.completion(
156
156
  messages=formatted_messages, **{**self._completion_fn_args, **kwargs}
157
157
  )
@@ -189,7 +189,7 @@ class LiteLLMRouterLLMClient(_BaseLiteLLMRouterClient, _BaseLiteLLMClient):
189
189
  if not self._use_chat_completions_endpoint:
190
190
  return await self._atext_completion(messages)
191
191
  try:
192
- formatted_messages = self._format_messages(messages)
192
+ formatted_messages = self._get_formatted_messages(messages)
193
193
  response = await self.router_client.acompletion(
194
194
  messages=formatted_messages, **{**self._completion_fn_args, **kwargs}
195
195
  )
rasa/shared/utils/llm.py CHANGED
@@ -23,6 +23,7 @@ from typing import (
23
23
  )
24
24
 
25
25
  import structlog
26
+ from jinja2 import Environment, select_autoescape
26
27
  from pydantic import BaseModel, Field
27
28
 
28
29
  import rasa.cli.telemetry
@@ -718,6 +719,38 @@ def embedder_client_factory(
718
719
  return client
719
720
 
720
721
 
722
+ def validate_jinja2_template(template_content: Text) -> None:
723
+ """Validate that a template string has valid Jinja2 syntax.
724
+
725
+ Args:
726
+ template_content: The template content to validate
727
+
728
+ Raises:
729
+ jinja2.exceptions.TemplateSyntaxError: If the template has invalid syntax
730
+ Exception: If there's an error during validation
731
+ """
732
+ # Create environment with custom filters
733
+ env = Environment(
734
+ autoescape=select_autoescape(
735
+ disabled_extensions=["jinja2"],
736
+ default_for_string=False,
737
+ default=True,
738
+ )
739
+ )
740
+ # Register filters - lazy import to avoid circular dependencies
741
+ from rasa.dialogue_understanding.generator._jinja_filters import (
742
+ to_json_escaped_string,
743
+ )
744
+ from rasa.dialogue_understanding.generator.constants import (
745
+ TO_JSON_ESCAPED_STRING_JINJA_FILTER,
746
+ )
747
+
748
+ env.filters[TO_JSON_ESCAPED_STRING_JINJA_FILTER] = to_json_escaped_string
749
+
750
+ # Validate Jinja2 syntax
751
+ env.from_string(template_content)
752
+
753
+
721
754
  def get_prompt_template(
722
755
  jinja_file_path: Optional[Text],
723
756
  default_prompt_template: Text,
@@ -1041,7 +1074,7 @@ def _get_llm_command_generator_config(
1041
1074
 
1042
1075
 
1043
1076
  def _get_compact_llm_command_generator_prompt(
1044
- config: Dict[Text, Any], endpoints: Dict[Text, Any]
1077
+ config: Dict[Text, Any], model_groups: Dict[Text, Any]
1045
1078
  ) -> Text:
1046
1079
  """Get the command generator prompt based on the config."""
1047
1080
  from rasa.dialogue_understanding.generator.single_step.compact_llm_command_generator import ( # noqa: E501
@@ -1051,7 +1084,7 @@ def _get_compact_llm_command_generator_prompt(
1051
1084
  model_config = _get_llm_command_generator_config(config)
1052
1085
  llm_config = resolve_model_client_config(
1053
1086
  model_config=model_config,
1054
- model_groups=endpoints.get(MODEL_GROUPS_CONFIG_KEY),
1087
+ model_groups=model_groups,
1055
1088
  )
1056
1089
  return get_default_prompt_template_based_on_model(
1057
1090
  llm_config=llm_config or {},
@@ -1086,7 +1119,7 @@ def get_system_default_prompts(
1086
1119
 
1087
1120
  Args:
1088
1121
  config: The config.yml file data.
1089
- endpoints: The endpoints.yml file data.
1122
+ endpoints: The endpoints configuration dictionary.
1090
1123
 
1091
1124
  Returns:
1092
1125
  SystemPrompts: A Pydantic model containing all default prompts.
@@ -1095,8 +1128,25 @@ def get_system_default_prompts(
1095
1128
  DEFAULT_RESPONSE_VARIATION_PROMPT_TEMPLATE,
1096
1129
  )
1097
1130
 
1131
+ # The Model Manager / Model API service receives the endpoints configuration
1132
+ # as raw YAML text rather than as a file path. However, both
1133
+ # Configuration.initialise_endpoints() and AvailableEndpoints.read_endpoints()
1134
+ # currently only accept a Path input and do not support loading from in-memory
1135
+ # YAML content.
1136
+
1137
+ # Since these classes only support file-based initialization today, we need
1138
+ # to bootstrap the Configuration with an empty AvailableEndpoints instance for now.
1139
+
1140
+ # IMPORTANT: This configuration must be properly initialized with valid endpoints
1141
+ # and available agents once Studio introduces full support for Agent configurations
1142
+ # (A2A and MCP).
1143
+ Configuration.initialise_empty()
1144
+
1145
+ model_groups = endpoints.get(MODEL_GROUPS_CONFIG_KEY)
1098
1146
  return SystemPrompts(
1099
- command_generator=_get_compact_llm_command_generator_prompt(config, endpoints),
1147
+ command_generator=_get_compact_llm_command_generator_prompt(
1148
+ config, model_groups
1149
+ ),
1100
1150
  enterprise_search=_get_enterprise_search_prompt(config),
1101
1151
  contextual_response_rephraser=DEFAULT_RESPONSE_VARIATION_PROMPT_TEMPLATE,
1102
1152
  )