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.
- rasa/agents/agent_manager.py +7 -5
- rasa/agents/protocol/a2a/a2a_agent.py +13 -11
- rasa/agents/protocol/mcp/mcp_base_agent.py +64 -12
- rasa/agents/validation.py +61 -6
- rasa/builder/copilot/constants.py +4 -0
- rasa/builder/copilot/copilot_templated_message_provider.py +24 -1
- rasa/builder/copilot/templated_messages/copilot_templated_responses.yml +3 -0
- rasa/builder/copilot/templated_messages/copilot_welcome_messages.yml +56 -0
- rasa/builder/jobs.py +162 -5
- rasa/builder/models.py +3 -0
- rasa/builder/validation_service.py +4 -0
- rasa/cli/arguments/data.py +9 -0
- rasa/cli/data.py +72 -6
- rasa/cli/interactive.py +3 -0
- rasa/cli/llm_fine_tuning.py +1 -0
- rasa/cli/project_templates/defaults.py +1 -0
- rasa/cli/validation/bot_config.py +4 -0
- rasa/constants.py +1 -1
- rasa/core/actions/action_exceptions.py +1 -1
- rasa/core/agent.py +4 -1
- rasa/core/available_agents.py +1 -1
- rasa/core/config/available_endpoints.py +6 -0
- rasa/core/exceptions.py +1 -1
- rasa/core/featurizers/tracker_featurizers.py +3 -2
- rasa/core/persistor.py +7 -7
- rasa/core/policies/flows/agent_executor.py +84 -4
- rasa/core/policies/flows/flow_exceptions.py +5 -2
- rasa/core/policies/flows/flow_executor.py +3 -2
- rasa/core/policies/flows/mcp_tool_executor.py +7 -1
- rasa/core/policies/rule_policy.py +1 -1
- rasa/dialogue_understanding/commands/cancel_flow_command.py +1 -1
- rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +22 -20
- rasa/engine/validation.py +6 -2
- rasa/model_manager/runner_service.py +1 -1
- rasa/privacy/privacy_config.py +1 -1
- rasa/shared/agents/auth/auth_strategy/oauth2_auth_strategy.py +4 -7
- rasa/shared/agents/auth/utils.py +85 -0
- rasa/shared/constants.py +3 -0
- rasa/shared/core/training_data/story_reader/story_reader.py +1 -1
- rasa/shared/exceptions.py +23 -2
- rasa/shared/providers/llm/litellm_router_llm_client.py +2 -2
- rasa/shared/utils/llm.py +54 -4
- rasa/shared/utils/mcp/server_connection.py +7 -4
- rasa/studio/download.py +3 -0
- rasa/studio/prompts.py +1 -0
- rasa/studio/upload.py +4 -0
- rasa/utils/log_utils.py +20 -1
- rasa/utils/pypred.py +7 -0
- rasa/validator.py +90 -2
- rasa/version.py +1 -1
- {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/METADATA +8 -7
- {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/RECORD +55 -53
- {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/NOTICE +0 -0
- {rasa_pro-3.14.0rc3.dist-info → rasa_pro-3.14.1.dist-info}/WHEEL +0 -0
- {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(
|
|
185
|
+
return _handle_agent_fatal_error(
|
|
186
|
+
output, final_events, stack, step, flows, tracker
|
|
187
|
+
)
|
|
181
188
|
else:
|
|
182
|
-
return _handle_agent_unknown_status(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
@@ -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
|
-
-
|
|
232
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
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(
|
|
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(
|
rasa/privacy/privacy_config.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
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
|
-
"""
|
|
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.
|
|
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.
|
|
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],
|
|
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=
|
|
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
|
|
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(
|
|
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
|
)
|