rasa-pro 3.14.0rc1__py3-none-any.whl → 3.14.0rc3__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 (84) hide show
  1. rasa/agents/protocol/a2a/a2a_agent.py +50 -42
  2. rasa/agents/utils.py +27 -5
  3. rasa/agents/validation.py +7 -9
  4. rasa/api.py +1 -2
  5. rasa/builder/copilot/constants.py +4 -1
  6. rasa/builder/copilot/copilot.py +191 -79
  7. rasa/builder/copilot/models.py +306 -116
  8. rasa/builder/copilot/prompts/copilot_system_prompt.jinja2 +33 -12
  9. rasa/builder/copilot/prompts/copilot_training_error_handler_prompt.jinja2 +53 -0
  10. rasa/builder/copilot/prompts/latest_user_message_context_prompt.jinja2 +59 -29
  11. rasa/builder/copilot/telemetry.py +8 -0
  12. rasa/builder/guardrails/policy_checker.py +1 -1
  13. rasa/builder/jobs.py +182 -12
  14. rasa/builder/models.py +12 -3
  15. rasa/builder/service.py +16 -2
  16. rasa/cli/dialogue_understanding_test.py +1 -0
  17. rasa/cli/e2e_test.py +1 -0
  18. rasa/cli/inspect.py +1 -0
  19. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/feedback.yml +46 -0
  20. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/goodbye.yml +9 -0
  21. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/help.yml +8 -0
  22. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/human_handoff.yml +41 -0
  23. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/patterns.yml +32 -0
  24. rasa/cli/project_templates/basic/tests/e2e_test_cases/without_stub/general/show_faqs.yml +8 -0
  25. rasa/cli/project_templates/finance/domain/general/help.yml +0 -0
  26. rasa/cli/project_templates/telco/data/network/flow_solve_internet_issue.yml +2 -2
  27. rasa/cli/project_templates/telco/domain/network/solve_internet_issue.yml +1 -2
  28. rasa/cli/project_templates/telco/tests/e2e_test_cases/with_stub/network/solve_internet_not_slow.yml +33 -0
  29. rasa/cli/project_templates/telco/tests/e2e_test_cases/with_stub/network/solve_internet_slow.yml +47 -0
  30. rasa/cli/project_templates/telco/tests/e2e_test_cases/without_stub/general/hello.yml +8 -0
  31. rasa/cli/run.py +1 -5
  32. rasa/cli/shell.py +1 -0
  33. rasa/cli/train.py +1 -0
  34. rasa/cli/validation/bot_config.py +7 -2
  35. rasa/core/available_agents.py +65 -55
  36. rasa/core/brokers/kafka.py +5 -1
  37. rasa/core/concurrent_lock_store.py +38 -21
  38. rasa/core/config/available_endpoints.py +0 -3
  39. rasa/core/config/configuration.py +36 -1
  40. rasa/core/constants.py +6 -0
  41. rasa/core/iam_credentials_providers/aws_iam_credentials_providers.py +69 -4
  42. rasa/core/iam_credentials_providers/credentials_provider_protocol.py +2 -1
  43. rasa/core/lock_store.py +4 -0
  44. rasa/core/policies/flows/agent_executor.py +16 -8
  45. rasa/core/redis_connection_factory.py +7 -2
  46. rasa/core/tracker_stores/redis_tracker_store.py +4 -0
  47. rasa/core/tracker_stores/sql_tracker_store.py +3 -1
  48. rasa/dialogue_understanding/commands/start_flow_command.py +10 -3
  49. rasa/dialogue_understanding/commands/utils.py +15 -4
  50. rasa/dialogue_understanding/generator/llm_based_command_generator.py +4 -2
  51. rasa/dialogue_understanding/generator/single_step/compact_llm_command_generator.py +4 -4
  52. rasa/dialogue_understanding/generator/single_step/search_ready_llm_command_generator.py +4 -4
  53. rasa/dialogue_understanding/generator/single_step/single_step_based_llm_command_generator.py +2 -2
  54. rasa/dialogue_understanding_test/du_test_runner.py +2 -2
  55. rasa/e2e_test/e2e_test_runner.py +2 -2
  56. rasa/shared/agents/auth/auth_strategy/oauth2_auth_strategy.py +10 -4
  57. rasa/shared/agents/auth/constants.py +1 -0
  58. rasa/shared/core/flows/steps/call.py +2 -2
  59. rasa/telemetry.py +3 -3
  60. rasa/validator.py +37 -0
  61. rasa/version.py +1 -1
  62. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/METADATA +14 -2
  63. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/RECORD +83 -73
  64. rasa/cli/project_templates/telco/tests/e2e_test_cases/network/solve_internet_issue.yml +0 -57
  65. /rasa/cli/project_templates/{finance/tests/e2e_test_cases → basic/tests/e2e_test_cases/without_stub}/general/hello.yml +0 -0
  66. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{accounts → without_stub/accounts}/check_balance.yml +0 -0
  67. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{accounts → without_stub/accounts}/download_statements.yml +0 -0
  68. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{cards → without_stub/cards}/block_card.yml +0 -0
  69. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/bot_challenge.yml +0 -0
  70. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/feedback.yml +0 -0
  71. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/goodbye.yml +0 -0
  72. /rasa/cli/project_templates/{telco/tests/e2e_test_cases → finance/tests/e2e_test_cases/without_stub}/general/hello.yml +0 -0
  73. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/human_handoff.yml +0 -0
  74. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{general → without_stub/general}/patterns.yml +0 -0
  75. /rasa/cli/project_templates/finance/tests/e2e_test_cases/{transfers → without_stub/transfers}/transfer_money.yml +0 -0
  76. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{billing → without_stub/billing}/understand_bill.yml +0 -0
  77. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/bot_challenge.yml +0 -0
  78. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/feedback.yml +0 -0
  79. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/goodbye.yml +0 -0
  80. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/human_handoff.yml +0 -0
  81. /rasa/cli/project_templates/telco/tests/e2e_test_cases/{general → without_stub/general}/patterns.yml +0 -0
  82. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/NOTICE +0 -0
  83. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/WHEEL +0 -0
  84. {rasa_pro-3.14.0rc1.dist-info → rasa_pro-3.14.0rc3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,41 @@
1
+ test_cases:
2
+ - test_case: human handoff happy path
3
+ steps:
4
+ - user: "I would like to speak with a human"
5
+ assertions:
6
+ - flow_started: "human_handoff"
7
+ - bot_uttered:
8
+ utter_name: "utter_ask_confirm_human_handoff"
9
+ buttons:
10
+ - payload: "/SetSlots(confirm_human_handoff=Yes)"
11
+ title: "Yes"
12
+ - payload: "/SetSlots(confirm_human_handoff=No)"
13
+ title: "No"
14
+ - user: "/SetSlots(confirm_human_handoff=Yes)"
15
+ assertions:
16
+ - slot_was_set:
17
+ - name: "confirm_human_handoff"
18
+ value: "Yes"
19
+ - bot_uttered:
20
+ utter_name: "utter_transferring_to_human"
21
+ - action_executed: action_human_handoff
22
+ # ====================================================>
23
+ - test_case: human handoff unhappy path
24
+ steps:
25
+ - user: "I would like to speak with a human"
26
+ assertions:
27
+ - flow_started: "human_handoff"
28
+ - bot_uttered:
29
+ utter_name: "utter_ask_confirm_human_handoff"
30
+ buttons:
31
+ - payload: "/SetSlots(confirm_human_handoff=Yes)"
32
+ title: "Yes"
33
+ - payload: "/SetSlots(confirm_human_handoff=No)"
34
+ title: "No"
35
+ - user: "/SetSlots(confirm_human_handoff=No)"
36
+ assertions:
37
+ - slot_was_set:
38
+ - name: "confirm_human_handoff"
39
+ value: "No"
40
+ - bot_uttered:
41
+ utter_name: "utter_human_handoff_cancelled"
@@ -0,0 +1,32 @@
1
+ test_cases:
2
+ - test_case: test pattern session start
3
+ steps:
4
+ - user: "/session_start"
5
+ assertions:
6
+ - flow_started: "pattern_session_start"
7
+ - bot_uttered:
8
+ utter_name: "utter_welcome"
9
+ #
10
+ - test_case: test basic conversation flow
11
+ steps:
12
+ - user: "hello"
13
+ assertions:
14
+ - flow_started: "hello"
15
+ - bot_uttered:
16
+ utter_name: "utter_hello"
17
+ - user: "what can you help me with?"
18
+ assertions:
19
+ - flow_started: "help"
20
+ - bot_uttered:
21
+ utter_name: "utter_what_can_you_do"
22
+ - user: "show me faqs"
23
+ assertions:
24
+ - flow_started: "show_faqs"
25
+ - bot_uttered:
26
+ utter_name: "utter_top_faqs"
27
+ - user: "goodbye"
28
+ assertions:
29
+ - flow_started: "goodbye_flow"
30
+ - bot_uttered:
31
+ utter_name: "utter_goodbye"
32
+ - flow_started: "leave_feedback"
@@ -0,0 +1,8 @@
1
+ test_cases:
2
+ - test_case: show faqs
3
+ steps:
4
+ - user: "show me the faqs"
5
+ assertions:
6
+ - flow_started: "show_faqs"
7
+ - bot_uttered:
8
+ utter_name: "utter_top_faqs"
@@ -69,5 +69,5 @@ flows:
69
69
  #when it is False this means this flow will only be run if it is called by another flow, not the user.
70
70
  description: run diagnostics to check the internet speed for the user
71
71
  steps:
72
- - action: utter_communicate_run_diagnosticss
73
- - action: actions_run_speed_test
72
+ - action: utter_communicate_run_diagnostics
73
+ - action: actions_run_speed_test
@@ -14,7 +14,7 @@ responses:
14
14
  - text: |
15
15
  I see.
16
16
  Let me try to identify the root cause of the issue you are facing.
17
- utter_communicate_run_diagnosticss:
17
+ utter_communicate_run_diagnostics:
18
18
  - text: |
19
19
  I will run a network diagnostics for you 🛜
20
20
  Wait a moment please...
@@ -72,4 +72,3 @@ responses:
72
72
  actions:
73
73
  - actions_run_speed_test
74
74
  - action_sleep_few_sec
75
-
@@ -0,0 +1,33 @@
1
+ stub_custom_actions:
2
+ actions_run_speed_test:
3
+ events:
4
+ - event: slot
5
+ name: network_speed
6
+ value: 150.0
7
+ responses:
8
+ - text: "Thank you for waiting... ✅ "
9
+ action_sleep_few_sec:
10
+ events: []
11
+ responses: []
12
+
13
+ test_cases:
14
+ - test_case: Solving Internet issue not slow
15
+ steps:
16
+ - user: "Hey, my internet is very slow. What's going on?"
17
+ assertions:
18
+ - flow_started: "fix_internet_slow"
19
+ - bot_uttered:
20
+ utter_name: "utter_acknowledge_issue"
21
+ - action_executed: action_sleep_few_sec
22
+ - flow_started: "run_diagnostics"
23
+ - bot_uttered:
24
+ utter_name: "utter_speed_network_not_issue"
25
+ - action_executed: "actions_run_speed_test"
26
+ - bot_uttered:
27
+ utter_name: "utter_propose_other_solutions"
28
+ - action_executed: "action_sleep_few_sec"
29
+ - bot_uttered:
30
+ utter_name: "utter_ask_more_help_needed"
31
+ - user: "/SetSlots(more_help_needed=True)"
32
+ assertions:
33
+ - flow_started: "human_handoff"
@@ -0,0 +1,47 @@
1
+ stub_custom_actions:
2
+ actions_run_speed_test:
3
+ events:
4
+ - event: slot
5
+ name: network_speed
6
+ value: 50.0
7
+ responses:
8
+ - text: "Thank you for waiting... ✅ "
9
+ action_sleep_few_sec:
10
+ events: []
11
+ responses: []
12
+
13
+ test_cases:
14
+ - test_case: Solving Internet slow issue
15
+ steps:
16
+ - user: "Hey, my internet is very slow. What's going on?"
17
+ assertions:
18
+ - flow_started: "fix_internet_slow"
19
+ - bot_uttered:
20
+ utter_name: "utter_acknowledge_issue"
21
+ - action_executed: action_sleep_few_sec
22
+ - flow_started: "run_diagnostics"
23
+ - bot_uttered:
24
+ utter_name: "utter_communicate_run_diagnostics"
25
+ - action_executed: "actions_run_speed_test"
26
+ - bot_uttered:
27
+ utter_name: "utter_acknowledge_speed_test_not_good"
28
+ - action_executed: "action_sleep_few_sec"
29
+ - bot_uttered:
30
+ utter_name: "utter_propose_reboot_router_solution"
31
+ - action_executed: "action_sleep_few_sec"
32
+ - flow_started: "reboot_router"
33
+ - bot_uttered:
34
+ utter_name: "utter_explain_reboot_router"
35
+ - bot_uttered:
36
+ utter_name: "utter_ask_reboot_router"
37
+ buttons:
38
+ - title: "Done"
39
+ payload: "/SetSlots(reboot_router=True)"
40
+ - title: "Facing a problem"
41
+ payload: "/SetSlots(reboot_router=False)"
42
+ - user: "/SetSlots(reboot_router=False)"
43
+ assertions:
44
+ - slot_was_set:
45
+ - name: "reboot_router"
46
+ value: False
47
+ - flow_started: "human_handoff"
@@ -0,0 +1,8 @@
1
+ test_cases:
2
+ - test_case: hello
3
+ steps:
4
+ - user: "hello"
5
+ assertions:
6
+ - flow_started: "hello"
7
+ - bot_uttered:
8
+ utter_name: "utter_hello"
rasa/cli/run.py CHANGED
@@ -88,7 +88,7 @@ def run(args: argparse.Namespace) -> None:
88
88
  """
89
89
  Configuration.initialise_endpoints(
90
90
  args.endpoints,
91
- ).initialise_credentials(
91
+ ).initialise_sub_agents(args.sub_agents).initialise_credentials(
92
92
  args.credentials,
93
93
  )
94
94
 
@@ -108,7 +108,6 @@ def run(args: argparse.Namespace) -> None:
108
108
  # configured
109
109
 
110
110
  import rasa.model
111
- from rasa.core.available_agents import AvailableAgents
112
111
 
113
112
  # start server if remote storage is configured
114
113
  if args.remote_storage is not None:
@@ -122,9 +121,6 @@ def run(args: argparse.Namespace) -> None:
122
121
  rasa_run(**vars(args))
123
122
  return
124
123
 
125
- # load sub-agents
126
- AvailableAgents.get_instance(args.sub_agents)
127
-
128
124
  # start server if local model found
129
125
  args.model = _validate_model_path(args.model, "model", DEFAULT_MODELS_PATH)
130
126
  local_model_set = True
rasa/cli/shell.py CHANGED
@@ -115,6 +115,7 @@ def shell(args: argparse.Namespace) -> None:
115
115
  # it can be used safely throughout the codebase with
116
116
  # `Configuration.get_instance().endpoints`
117
117
  Configuration.initialise_endpoints(endpoints_path=Path(args.endpoints))
118
+ Configuration.initialise_sub_agents(args.sub_agents)
118
119
  model = get_validated_path(args.model, "model", DEFAULT_MODELS_PATH)
119
120
 
120
121
  try:
rasa/cli/train.py CHANGED
@@ -128,6 +128,7 @@ def run_training(args: argparse.Namespace, can_exit: bool = False) -> Optional[T
128
128
  # Validates and loads endpoints with proper endpoint file location
129
129
  # TODO(Radovan): this should be probably be done in Configuration
130
130
  _check_nlg_endpoint_validity(args.endpoints)
131
+ Configuration.initialise_sub_agents(args.sub_agents)
131
132
 
132
133
  training_files = [
133
134
  get_validated_path(f, "data", DEFAULT_DATA_PATH, none_is_valid=True)
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Optional
5
5
  import structlog
6
6
 
7
7
  from rasa import telemetry
8
+ from rasa.core.constants import DEFAULT_SUB_AGENTS
8
9
  from rasa.exceptions import ValidationError
9
10
  from rasa.shared.importers.importer import TrainingDataImporter
10
11
  from rasa.shared.utils.common import display_research_study_prompt
@@ -194,8 +195,12 @@ def validate_files(
194
195
  valid_translations = True
195
196
  valid_CALM_slot_mappings = validator.validate_CALM_slot_mappings()
196
197
 
197
- # Validate sub-agents if specified
198
- valid_sub_agents = _validate_sub_agents(sub_agents) if sub_agents else True
198
+ # Validate sub-agents
199
+ sub_agents_path = sub_agents or DEFAULT_SUB_AGENTS
200
+ valid_sub_agents = _validate_sub_agents(sub_agents_path)
201
+
202
+ if valid_sub_agents:
203
+ valid_sub_agents = validator.validate_agent_flow_conflicts(sub_agents_path)
199
204
 
200
205
  all_good = (
201
206
  valid_domain
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, model_validator
9
9
  from ruamel import yaml as yaml
10
10
 
11
11
  from rasa.exceptions import ValidationError
12
- from rasa.utils.singleton import Singleton
12
+ from rasa.shared.utils.yaml import read_config_file
13
13
 
14
14
  DEFAULT_AGENTS_CONFIG_FOLDER = "sub_agents"
15
15
 
@@ -89,48 +89,68 @@ class AgentConfig(BaseModel):
89
89
  connections: Optional[AgentConnections] = None
90
90
 
91
91
 
92
- class AvailableAgents(metaclass=Singleton):
92
+ class AvailableAgents:
93
93
  """Collection of configured agents."""
94
94
 
95
- _instance = None
96
-
97
95
  def __init__(self, agents: Optional[Dict[str, AgentConfig]] = None) -> None:
98
96
  """Create an `AvailableAgents` object."""
99
- self.agents = agents or {}
97
+ self.agents: Dict[str, AgentConfig] = agents or {}
100
98
 
101
99
  @classmethod
102
- def _read_agent_folder(cls, agent_folder: str) -> AvailableAgents:
100
+ def read_from_folder(cls, sub_agents_folder: str) -> AvailableAgents:
103
101
  """Read the different agents from the given folder."""
104
102
  agents: Dict[str, AgentConfig] = {}
105
103
 
106
- if not os.path.isdir(agent_folder):
107
- if agent_folder != DEFAULT_AGENTS_CONFIG_FOLDER:
104
+ if not os.path.isdir(sub_agents_folder):
105
+ if sub_agents_folder != DEFAULT_AGENTS_CONFIG_FOLDER:
108
106
  # User explicitly specified a folder, it should exist
109
- structlogger.error(
110
- f"The specified agents config folder '{agent_folder}' does not "
111
- f"exist or is not a directory."
112
- )
113
- raise ValueError(
114
- f"The specified agents config folder '{agent_folder}' does not "
115
- f"exist or is not a directory."
107
+ raise ValidationError(
108
+ code="agent.sub_agents_folder_not_found",
109
+ event_info=f"The specified agents config folder "
110
+ f"'{sub_agents_folder}' does not exist or is not a "
111
+ f"directory.",
112
+ details={"folder": sub_agents_folder},
116
113
  )
117
114
  else:
118
115
  # We are using the default folder, it may not be created yet
119
116
  # Init with an empty agents in this case
120
117
  structlogger.info(
121
- f"Default agents config folder '{agent_folder}' does not exist. "
122
- f"Agent configurations won't be loaded."
118
+ f"Default agents config folder '{sub_agents_folder}' does not "
119
+ f"exist. Agent configurations won't be loaded."
123
120
  )
124
121
  return cls(agents)
125
122
 
126
123
  # First, load all agent configs into a temporary list for validation
127
124
  agent_configs: List[AgentConfig] = []
128
- for agent_name in os.listdir(agent_folder):
129
- config_path = os.path.join(agent_folder, agent_name, "config.yml")
125
+ for agent_name in os.listdir(sub_agents_folder):
126
+ agent_folder = os.path.join(sub_agents_folder, agent_name)
127
+ if not os.path.isdir(agent_folder):
128
+ raise ValidationError(
129
+ code="agent.invalid_directory_structure",
130
+ event_info=f"Invalid structure: '{agent_folder}' is not a folder. "
131
+ f"Each agent must be stored in its own folder inside "
132
+ f"'{sub_agents_folder}'. Expected structure: "
133
+ f"{sub_agents_folder}/<agent_name>/config.yml",
134
+ details={
135
+ "agent_name": agent_name,
136
+ "sub_agents_folder": sub_agents_folder,
137
+ },
138
+ )
139
+ config_path = os.path.join(agent_folder, "config.yml")
130
140
  if not os.path.isfile(config_path):
131
- continue
141
+ raise ValidationError(
142
+ code="agent.missing_config_file",
143
+ event_info=f"Missing config file for agent '{agent_name}'. "
144
+ f"Expected file: '{config_path}'. "
145
+ f"Each agent folder must contain a 'config.yml' file.",
146
+ details={
147
+ "agent_name": agent_name,
148
+ "expected_config_file": config_path,
149
+ "sub_agents_folder": sub_agents_folder,
150
+ },
151
+ )
132
152
  try:
133
- agent_config = cls._read_agent_config(config_path)
153
+ agent_config = cls._read_agent_config_file(config_path)
134
154
  if not isinstance(agent_config, AgentConfig):
135
155
  raise ValueError(f"Invalid agent config type for {agent_name}")
136
156
  agent_configs.append(agent_config)
@@ -140,7 +160,7 @@ class AvailableAgents(metaclass=Singleton):
140
160
  event_info=f"Failed to load agent '{agent_name}': {e}",
141
161
  details={
142
162
  "agent_name": agent_name,
143
- "agent_folder": agent_folder,
163
+ "sub_agents_folder": sub_agents_folder,
144
164
  "error": str(e),
145
165
  },
146
166
  )
@@ -153,27 +173,23 @@ class AvailableAgents(metaclass=Singleton):
153
173
  for agent_config in agent_configs:
154
174
  agents[agent_config.agent.name] = agent_config
155
175
 
176
+ structlogger.info(f"Loaded agent configs: {[k for k in agents.keys()]}")
156
177
  return cls(agents)
157
178
 
158
179
  @staticmethod
159
- def _read_agent_config(config_path: str) -> AgentConfig:
160
- """Read the agent config from a yaml file into Pydantic models.
180
+ def from_dict(data: Dict[str, Any]) -> AgentConfig:
181
+ """Parse the agent config from raw data into Pydantic models.
161
182
 
162
183
  Args:
163
- config_path: Path to the config file.
184
+ data: Raw data from the config file as a dictionary.
164
185
 
165
186
  Returns:
166
- The parsed AgentConfig.
187
+ The parsed AgentConfig as a Pydantic model.
167
188
 
168
189
  Raises:
169
- yaml.YAMLError: If the YAML file is invalid.
170
190
  ValueError: If the data structure is invalid for Pydantic models.
171
191
  """
172
- with open(config_path, "r") as f:
173
- data = yaml.safe_load(f)
174
-
175
- # Create the agent config (this will trigger Pydantic validation)
176
- agent_config = AgentConfig(
192
+ return AgentConfig(
177
193
  agent=AgentInfo(**data.get("agent", {})),
178
194
  configuration=AgentConfiguration(**data.get("configuration", {}))
179
195
  if data.get("configuration")
@@ -183,24 +199,22 @@ class AvailableAgents(metaclass=Singleton):
183
199
  else None,
184
200
  )
185
201
 
186
- return agent_config
187
-
188
202
  @classmethod
189
- def get_instance(
190
- cls, agent_folder: Optional[str] = DEFAULT_AGENTS_CONFIG_FOLDER
191
- ) -> AvailableAgents:
192
- """Get the singleton instance of `AvailableAgents`."""
193
- if cls._instance is None:
194
- cls._instance = cls._read_agent_folder(agent_folder)
203
+ def _read_agent_config_file(cls, config_path: str) -> AgentConfig:
204
+ """Read the agent config from a yaml file into Pydantic models.
195
205
 
196
- return cls._instance
206
+ Args:
207
+ config_path: Path to the config file.
197
208
 
198
- @classmethod
199
- def reset_instance(cls) -> None:
200
- cls._instance = None
201
- # Also clear the metaclass singleton instances
202
- if hasattr(type(cls), "_instances"):
203
- type(cls)._instances.clear()
209
+ Returns:
210
+ The parsed AgentConfig.
211
+
212
+ Raises:
213
+ yaml.YAMLError: If the YAML file is invalid.
214
+ ValidationError: If the data structure is invalid for Pydantic models.
215
+ """
216
+ data = read_config_file(config_path)
217
+ return cls.from_dict(data)
204
218
 
205
219
  def as_json_list(self) -> List[Dict[str, Any]]:
206
220
  """Convert the available agents to a JSON-serializable list."""
@@ -218,12 +232,8 @@ class AvailableAgents(metaclass=Singleton):
218
232
  for agent_name, agent_config in self.agents.items()
219
233
  ]
220
234
 
221
- @classmethod
222
- def get_agent_config(cls, agent_id: str) -> Optional[AgentConfig]:
223
- instance = cls.get_instance()
224
- return instance.agents.get(agent_id)
235
+ def get_agent_config(self, agent_id: str) -> Optional[AgentConfig]:
236
+ return self.agents.get(agent_id)
225
237
 
226
- @classmethod
227
- def has_agents(cls) -> bool:
228
- instance = cls.get_instance()
229
- return len(instance.agents) > 0
238
+ def has_agents(self) -> bool:
239
+ return len(self.agents) > 0
@@ -12,6 +12,7 @@ import structlog
12
12
 
13
13
  import rasa.shared.utils.common
14
14
  from rasa.core.brokers.broker import EventBroker
15
+ from rasa.core.constants import KAFKA_SERVICE_NAME
15
16
  from rasa.core.exceptions import KafkaProducerInitializationError
16
17
  from rasa.core.iam_credentials_providers.credentials_provider_protocol import (
17
18
  IAMCredentialsProviderInput,
@@ -99,7 +100,10 @@ class KafkaEventBroker(EventBroker):
99
100
  self.queue_size = kwargs.get("queue_size")
100
101
  self.ssl_check_hostname = "https" if ssl_check_hostname else None
101
102
  self.iam_credentials_provider = create_iam_credentials_provider(
102
- IAMCredentialsProviderInput(service_name=SupportedServiceType.EVENT_BROKER)
103
+ IAMCredentialsProviderInput(
104
+ service_type=SupportedServiceType.EVENT_BROKER,
105
+ service_name=KAFKA_SERVICE_NAME,
106
+ )
103
107
  )
104
108
 
105
109
  # PII management attributes
@@ -6,6 +6,9 @@ from typing import Deque, Optional, Text
6
6
  import structlog
7
7
  from pydantic import ValidationError
8
8
 
9
+ from rasa.core.iam_credentials_providers.credentials_provider_protocol import (
10
+ SupportedServiceType,
11
+ )
9
12
  from rasa.core.lock import Ticket, TicketLock
10
13
  from rasa.core.lock_store import (
11
14
  DEFAULT_SOCKET_TIMEOUT_IN_SECONDS,
@@ -108,6 +111,7 @@ class ConcurrentRedisLockStore(LockStore):
108
111
  redis_config = RedisConfig(
109
112
  host=host,
110
113
  port=port,
114
+ service_type=SupportedServiceType.LOCK_STORE,
111
115
  db=db,
112
116
  username=username,
113
117
  password=password,
@@ -150,32 +154,45 @@ class ConcurrentRedisLockStore(LockStore):
150
154
  ),
151
155
  )
152
156
 
153
- def _get_keys_by_pattern(self, pattern: Text) -> list:
154
- """Get keys by pattern, using SCAN for cluster mode and KEYS for others."""
155
- if self.deployment_mode == DeploymentMode.CLUSTER.value:
156
- # In cluster mode, use SCAN to get keys more reliably
157
- keys = []
158
- cursor = 0
159
-
160
- while True:
161
- try:
162
- cursor, batch_keys = self.red.scan(cursor, match=pattern, count=100)
163
- keys.extend(batch_keys)
157
+ def _scan_cluster_keys(self, pattern: Text) -> list:
158
+ """Scan keys in cluster mode with proper cursor handling."""
159
+ keys = []
160
+ cursor = 0
161
+
162
+ while True:
163
+ try:
164
+ cursor, batch_keys = self.red.scan(cursor, match=pattern, count=100)
165
+ keys.extend(batch_keys)
166
+
167
+ if isinstance(cursor, dict):
168
+ # cursor is a dict mapping each node to its scan position. e.g
169
+ # {'127.0.0.1:7000': 0, '127.0.0.1:7001': 5, '127.0.0.1:7002': 0}
170
+ # A cursor value of 0 means that node has finished scanning
171
+ # When all nodes show 0, the entire cluster scan is complete
172
+ if all(v == 0 for v in cursor.values()):
173
+ break
174
+ else:
175
+ # if scan is complete
164
176
  if cursor == 0:
165
177
  break
166
- except Exception as e:
167
- structlogger.warning(
168
- "concurrent_redis_lock_store._get_keys_by_pattern.scan_interrupted",
169
- event_info=f"SCAN interrupted in cluster mode: {e}. "
170
- f"Returning {len(keys)} keys found so far.",
171
- )
172
- break
173
- else:
174
- # Standard and sentinel modes use KEYS
175
- keys = self.red.keys(pattern)
178
+
179
+ except Exception as e:
180
+ structlogger.warning(
181
+ "concurrent_redis_lock_store._get_keys_by_pattern.scan_interrupted",
182
+ event_info=f"SCAN interrupted in cluster mode: {e}. "
183
+ f"Returning {len(keys)} keys found so far.",
184
+ )
185
+ break
176
186
 
177
187
  return keys
178
188
 
189
+ def _get_keys_by_pattern(self, pattern: Text) -> list:
190
+ """Get keys by pattern, using SCAN for cluster mode and KEYS for others."""
191
+ if self.deployment_mode == DeploymentMode.CLUSTER.value:
192
+ return self._scan_cluster_keys(pattern)
193
+ else:
194
+ return self.red.keys(pattern)
195
+
179
196
  def issue_ticket(
180
197
  self, conversation_id: Text, lock_lifetime: float = LOCK_LIFETIME
181
198
  ) -> int:
@@ -94,12 +94,9 @@ class MCPServerConfig(BaseModel):
94
94
  class AvailableEndpoints:
95
95
  """Collection of configured endpoints."""
96
96
 
97
- _instance = None
98
-
99
97
  @classmethod
100
98
  def read_endpoints(cls, endpoint_file: Path) -> AvailableEndpoints:
101
99
  """Read the different endpoints from a yaml file."""
102
-
103
100
  nlg = read_endpoint_config(endpoint_file, endpoint_type="nlg")
104
101
  nlu = read_endpoint_config(endpoint_file, endpoint_type="nlu")
105
102
  action = read_endpoint_config(endpoint_file, endpoint_type="action_endpoint")