Skip to content

Commit 605ea2c

Browse files
authored
refactor(actions): migrate built-in rails to RailOutcome (#2151)
Migrate built-in rail actions and streaming bypasses from legacy output-mapping returns to the engine-neutral RailOutcome contract. Remove the legacy fallback and add runtime-flow/equivalence coverage for the migrated rails. Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
1 parent 1e86f75 commit 605ea2c

149 files changed

Lines changed: 5625 additions & 1613 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/configure-rails/guardrail-catalog/community/f5-ai-guardrails.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ To customize the behavior, you can overwrite the default flows in your configura
145145
define subflow f5 guardrails scan input
146146
$result = execute f5_guardrails_scan(text=$user_message)
147147
148-
if $result.result.outcome != "cleared"
148+
if $result.is_blocked
149149
bot say "I cannot process this request due to safety policies."
150150
stop
151151
```

nemoguardrails/actions/actions.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ class ActionMeta(TypedDict):
3030
name: str
3131
is_system_action: bool
3232
execute_async: bool
33-
output_mapping: Optional[Callable[[Any], bool]]
3433

3534

3635
# Create a TypeVar to represent the decorated function or class
@@ -41,17 +40,13 @@ def action(
4140
is_system_action: bool = False,
4241
name: Optional[str] = None,
4342
execute_async: bool = False,
44-
output_mapping: Optional[Callable[[Any], bool]] = None,
4543
) -> Callable[[T], T]:
4644
"""Decorator to mark a function or class as an action.
4745
4846
Args:
4947
is_system_action (bool): Flag indicating if the action is a system action.
5048
name (str): The name to associate with the action.
5149
execute_async: Whether the function should be executed in async mode.
52-
output_mapping (Optional[Callable[[Any], bool]]): A function to interpret the action's result.
53-
It accepts the return value (e.g. the first element of a tuple) and return True if the output
54-
is not safe.
5550
5651
Returns:
5752
callable: The decorated function or class.
@@ -74,7 +69,6 @@ def decorator(fn_or_cls: T) -> T:
7469
"name": action_name,
7570
"is_system_action": is_system_action,
7671
"execute_async": execute_async,
77-
"output_mapping": output_mapping,
7872
}
7973

8074
setattr(fn_or_cls_target, "action_meta", action_meta)

nemoguardrails/actions/output_mapping.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

nemoguardrails/colang/v1_0/runtime/runtime.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from nemoguardrails.actions.actions import ActionResult
2727
from nemoguardrails.actions.core import create_event
28-
from nemoguardrails.actions.output_mapping import is_output_blocked
28+
from nemoguardrails.actions.rail_outcome import require_rail_outcome
2929
from nemoguardrails.colang import parse_colang_file
3030
from nemoguardrails.colang.runtime import Runtime
3131
from nemoguardrails.colang.v1_0.runtime.flows import (
@@ -488,11 +488,7 @@ async def run_single_rail(flow_id: str, action_info: dict) -> tuple:
488488
log.error(error_msg)
489489
return flow_id, "internal_error", error_msg
490490

491-
action_func = self.action_dispatcher.get_action(action_name)
492-
493-
# use the mapping to decide if the result indicates blocked content.
494-
# True means blocked, False means allowed
495-
result = is_output_blocked(result, action_func)
491+
result = require_rail_outcome(result).is_blocked
496492

497493
return flow_id, result, None
498494

nemoguardrails/library/activefence/actions.py

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,62 +15,66 @@
1515
import json
1616
import logging
1717
import os
18-
from typing import Optional
18+
from typing import Literal, Optional
1919

2020
import aiohttp
2121

2222
from nemoguardrails.actions import action
23+
from nemoguardrails.actions.rail_outcome import RailOutcome
2324
from nemoguardrails.utils import new_uuid
2425

2526
log = logging.getLogger(__name__)
2627

27-
28-
def call_activefence_api_mapping(result: dict) -> bool:
29-
"""
30-
Mapping for call_activefence_api.
31-
32-
Expects result to be a dict with:
33-
- "max_risk_score": a float representing the overall risk.
34-
- "violations": a dict mapping violation type (str) to risk score (float).
35-
36-
Block (return True) if any of the following conditions are met:
37-
1. Overall max risk score > 0.7.
38-
2. Individual thresholds are exceeded:
39-
- "abusive_or_harmful.harassment_or_bullying" > 0.8
40-
- "abusive_or_harmful.profanity" > 0.7
41-
- "abusive_or_harmful.hate_speech" > 0.6
42-
- "abusive_or_harmful.child_grooming" > 0.4
43-
- "abusive_or_harmful.general_violence" > 0.7
44-
- "self_harm.general" > 0.8
45-
- "adult_content.general" > 0.3
46-
- "privacy_violation.pii" > 0.8
47-
"""
48-
max_risk = result.get("max_risk_score", 0.0)
49-
if max_risk > 0.7:
50-
return True
51-
52-
violations = result.get("violations", {})
53-
54-
thresholds = {
55-
"abusive_or_harmful.harassment_or_bullying": 0.8,
56-
"abusive_or_harmful.profanity": 0.7,
57-
"abusive_or_harmful.hate_speech": 0.6,
58-
"abusive_or_harmful.child_grooming": 0.4,
59-
"abusive_or_harmful.general_violence": 0.7,
60-
"self_harm.general": 0.8,
61-
"adult_content.general": 0.3,
62-
"privacy_violation.pii": 0.8,
28+
ACTIVEFENCE_DETAILED_THRESHOLDS = {
29+
"abusive_or_harmful.harassment_or_bullying": 0.8,
30+
"abusive_or_harmful.profanity": 0.7,
31+
"abusive_or_harmful.hate_speech": 0.6,
32+
"abusive_or_harmful.child_grooming": 0.4,
33+
"abusive_or_harmful.general_violence": 0.7,
34+
"self_harm.general": 0.8,
35+
"adult_content.general": 0.3,
36+
"privacy_violation.pii": 0.8,
37+
}
38+
39+
40+
def _activefence_simple_blocked(max_risk_score: float) -> bool:
41+
return max_risk_score > 0.7
42+
43+
44+
def _activefence_detailed_blocked(violations: dict[str, float]) -> bool:
45+
return any(
46+
violations.get(violation_type, 0) > threshold
47+
for violation_type, threshold in ACTIVEFENCE_DETAILED_THRESHOLDS.items()
48+
)
49+
50+
51+
def _activefence_outcome(
52+
max_risk_score: float,
53+
violations: dict[str, float],
54+
threshold_mode: Literal["simple", "detailed"] = "simple",
55+
) -> RailOutcome:
56+
metadata = {
57+
"max_risk_score": max_risk_score,
58+
"violations": violations,
59+
"threshold_mode": threshold_mode,
6360
}
64-
65-
for violation_type, threshold in thresholds.items():
66-
if violations.get(violation_type, 0) > threshold:
67-
return True
68-
69-
return False
70-
71-
72-
@action(is_system_action=True, output_mapping=call_activefence_api_mapping)
73-
async def call_activefence_api(text: Optional[str] = None, **kwargs):
61+
blocked = (
62+
_activefence_detailed_blocked(violations)
63+
if threshold_mode == "detailed"
64+
else _activefence_simple_blocked(max_risk_score)
65+
)
66+
67+
if blocked:
68+
return RailOutcome.block(metadata=metadata)
69+
return RailOutcome.allow(metadata=metadata)
70+
71+
72+
@action(is_system_action=True)
73+
async def call_activefence_api(
74+
text: Optional[str] = None,
75+
threshold_mode: Literal["simple", "detailed"] = "simple",
76+
**kwargs,
77+
) -> RailOutcome:
7478
api_key = os.environ.get("ACTIVEFENCE_API_KEY")
7579

7680
if api_key is None:
@@ -104,4 +108,4 @@ async def call_activefence_api(text: Optional[str] = None, **kwargs):
104108
max_risk_score = violation["risk_score"]
105109
violations_dict[violation["violation_type"]] = violation["risk_score"]
106110

107-
return {"max_risk_score": max_risk_score, "violations": violations_dict}
111+
return _activefence_outcome(max_risk_score, violations_dict, threshold_mode)

nemoguardrails/library/activefence/flows.co

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ Privacy Violation PII
1515

1616
flow activefence moderation on input
1717
"""Guardrail based on the maximum risk score."""
18-
$result = await CallActivefenceApiAction(text=$user_message)
18+
$result = await CallActivefenceApiAction(text=$user_message, threshold_mode="simple")
1919

20-
if $result.max_risk_score > 0.7
20+
if $result.is_blocked
2121
if $system.config.enable_rails_exceptions
2222
send ActiveFenceModerationRailException(message="ActiveFence moderation triggered. The maximum risk score exceeded the threshold.")
2323
else
@@ -26,9 +26,9 @@ flow activefence moderation on input
2626

2727
flow activefence moderation on output
2828
"""Guardrail based on the maximum risk score."""
29-
$result = await CallActivefenceApiAction(text=$bot_message)
29+
$result = await CallActivefenceApiAction(text=$bot_message, threshold_mode="simple")
3030

31-
if $result.max_risk_score > 0.7
31+
if $result.is_blocked
3232
if $system.config.enable_rails_exceptions
3333
send ActiveFenceModerationRailException(message="ActiveFence moderation triggered. The maximum risk score exceeded the threshold.")
3434
else
@@ -37,58 +37,58 @@ flow activefence moderation on output
3737

3838
flow activefence moderation on input detailed
3939
"""Guardrail based on individual risk scores."""
40-
$result = await CallActivefenceApiAction(text=$user_message)
40+
$result = await CallActivefenceApiAction(text=$user_message, threshold_mode="detailed")
4141

42-
if $result.violations.get("abusive_or_harmful.harassment_or_bullying", 0) > 0.8
42+
if $result.metadata["violations"].get("abusive_or_harmful.harassment_or_bullying", 0) > 0.8
4343
if $system.config.enable_rails_exceptions
4444
send ActiveFenceHarassmentRailException(message="ActiveFence moderation triggered. The harassment or bullying risk score exceeded the threshold.")
4545
else
4646
bot inform cannot engage in abusive_or_harmful behavior
4747
abort
4848

49-
if $result.violations.get("abusive_or_harmful.profanity", 0) > 0.7
49+
if $result.metadata["violations"].get("abusive_or_harmful.profanity", 0) > 0.7
5050
if $system.config.enable_rails_exceptions
5151
send ActiveFenceProfanityRailException(message="ActiveFence moderation triggered. The profanity risk score exceeded the threshold.")
5252
else
5353
bot inform cannot engage in abusive_or_harmful behavior
5454
abort
5555

56-
if $result.violations.get("abusive_or_harmful.hate_speech", 0) > 0.6
56+
if $result.metadata["violations"].get("abusive_or_harmful.hate_speech", 0) > 0.6
5757
if $system.config.enable_rails_exceptions
5858
send ActiveFenceHateSpeechRailException(message="ActiveFence moderation triggered. The hate speech risk score exceeded the threshold.")
5959
else
6060
bot inform cannot engage in abusive_or_harmful behavior
6161
abort
6262

63-
if $result.violations.get("abusive_or_harmful.child_grooming", 0) > 0.4
63+
if $result.metadata["violations"].get("abusive_or_harmful.child_grooming", 0) > 0.4
6464
if $system.config.enable_rails_exceptions
6565
send ActiveFenceChildGroomingRailException(message="ActiveFence moderation triggered. The child grooming risk score exceeded the threshold.")
6666
else
6767
bot inform cannot engage in abusive_or_harmful behavior
6868
abort
6969

70-
if $result.violations.get("abusive_or_harmful.general_violence", 0) > 0.7
70+
if $result.metadata["violations"].get("abusive_or_harmful.general_violence", 0) > 0.7
7171
if $system.config.enable_rails_exceptions
7272
send ActiveFenceGeneralViolenceRailException(message="ActiveFence moderation triggered. The general violence risk score exceeded the threshold.")
7373
else
7474
bot inform cannot engage in abusive_or_harmful behavior
7575
abort
7676

77-
if $result.violations.get("self_harm.general", 0) > 0.8
77+
if $result.metadata["violations"].get("self_harm.general", 0) > 0.8
7878
if $system.config.enable_rails_exceptions
7979
send ActiveFenceSelfHarmRailException(message="ActiveFence moderation triggered. The self harm risk score exceeded the threshold.")
8080
else
8181
bot inform cannot engage in self harm behavior
8282
abort
8383

84-
if $result.violations.get("adult_content.general", 0) > 0.3
84+
if $result.metadata["violations"].get("adult_content.general", 0) > 0.3
8585
if $system.config.enable_rails_exceptions
8686
send ActiveFenceAdultContentRailException(message="ActiveFence moderation triggered. The adult content risk score exceeded the threshold.")
8787
else
8888
bot inform cannot engage with inappropriate content
8989
abort
9090

91-
if $result.violations.get("privacy_violation.pii", 0) > 0.8
91+
if $result.metadata["violations"].get("privacy_violation.pii", 0) > 0.8
9292
if $system.config.enable_rails_exceptions
9393
send ActiveFencePrivacyViolationRailException(message="ActiveFence moderation triggered. The privacy violation risk score exceeded the threshold.")
9494
else

0 commit comments

Comments
 (0)