Skip to content

Commit 97da452

Browse files
authored
fix(telemetry): derive built-in features from manifests (#2301)
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
1 parent 36c1301 commit 97da452

3 files changed

Lines changed: 159 additions & 78 deletions

File tree

docs/telemetry.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ In this context, a *session* is the lifetime of a single Python process running
4141

4242
### Possible Values for Built-In Features
4343

44-
Each value corresponds to a directory under `nemoguardrails/library/`. This list reflects the current release; the directories under `nemoguardrails/library/` are the authoritative source.
44+
Each value identifies a built-in library feature. The built-in rail manifests are the authoritative source. Related manifests can share a stable feature identifier so telemetry remains comparable across releases.
4545

46-
`activefence`, `ai_defense`, `autoalign`, `clavata`, `cleanlab`, `content_safety`, `crowdstrike_aidr`, `factchecking`, `fiddler`, `gliner`, `guardrails_ai`, `hallucination`, `injection_detection`, `jailbreak_detection`, `llama_guard`, `pangea`, `patronusai`, `policyai`, `prompt_security`, `regex`, `self_check`, `sensitive_data_detection`, `topic_safety`, `trend_micro`.
46+
`activefence`, `ai_defense`, `autoalign`, `clavata`, `cleanlab`, `content_safety`, `context_bloat_detection`, `crowdstrike_aidr`, `f5`, `factchecking`, `fiddler`, `gcp_moderate_text`, `gliner`, `guardrails_ai`, `hallucination`, `hf_classifier`, `injection_detection`, `jailbreak_detection`, `llama_guard`, `pangea`, `patronusai`, `policyai`, `polygraf`, `prompt_security`, `regex`, `self_check`, `sensitive_data_detection`, `topic_safety`, `trend_micro`.
4747

4848
## Data Not Collected by Telemetry
4949

nemoguardrails/telemetry.py

Lines changed: 29 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -387,86 +387,23 @@ def _is_usage_stats_enabled() -> bool:
387387
return True
388388

389389

390-
_KNOWN_BUILTIN_FLOWS = {
391-
"activefence moderation on input": "activefence",
392-
"activefence moderation on input detailed": "activefence",
393-
"activefence moderation on output": "activefence",
394-
"ai defense inspect prompt": "ai_defense",
395-
"ai defense inspect response": "ai_defense",
396-
"alignscore check facts": "factchecking",
397-
"autoalign check input": "autoalign",
398-
"autoalign check output": "autoalign",
399-
"autoalign factcheck output": "autoalign",
400-
"autoalign groundedness output": "autoalign",
401-
"clavata check for": "clavata",
402-
"clavata check input": "clavata",
403-
"clavata check output": "clavata",
404-
"cleanlab trustworthiness": "cleanlab",
405-
"content safety check input": "content_safety",
406-
"content safety check output": "content_safety",
407-
"crowdstrike aidr guard input": "crowdstrike_aidr",
408-
"crowdstrike aidr guard output": "crowdstrike_aidr",
409-
"detect pii on input": "sensitive_data_detection",
410-
"detect pii on output": "sensitive_data_detection",
411-
"detect pii on retrieval": "sensitive_data_detection",
412-
"detect sensitive data on input": "sensitive_data_detection",
413-
"detect sensitive data on output": "sensitive_data_detection",
414-
"detect sensitive data on retrieval": "sensitive_data_detection",
415-
"fiddler bot faithfulness": "fiddler",
416-
"fiddler bot safety": "fiddler",
417-
"fiddler user safety": "fiddler",
418-
"gliner detect pii on input": "gliner",
419-
"gliner detect pii on output": "gliner",
420-
"gliner detect pii on retrieval": "gliner",
421-
"gliner mask pii on input": "gliner",
422-
"gliner mask pii on output": "gliner",
423-
"gliner mask pii on retrieval": "gliner",
424-
"guardrailsai check input": "guardrails_ai",
425-
"guardrailsai check output": "guardrails_ai",
426-
"hallucination warning": "hallucination",
427-
"injection detection": "injection_detection",
428-
"jailbreak detection heuristics": "jailbreak_detection",
429-
"jailbreak detection model": "jailbreak_detection",
430-
"llama guard check input": "llama_guard",
431-
"llama guard check output": "llama_guard",
432-
"mask pii on input": "sensitive_data_detection",
433-
"mask pii on output": "sensitive_data_detection",
434-
"mask pii on retrieval": "sensitive_data_detection",
435-
"mask sensitive data on input": "sensitive_data_detection",
436-
"mask sensitive data on output": "sensitive_data_detection",
437-
"mask sensitive data on retrieval": "sensitive_data_detection",
438-
"pangea ai guard input": "pangea",
439-
"pangea ai guard output": "pangea",
440-
"patronus api check output": "patronusai",
441-
"patronus lynx check output hallucination": "patronusai",
442-
"policyai moderation on input": "policyai",
443-
"policyai moderation on output": "policyai",
444-
"protect prompt": "prompt_security",
445-
"protect response": "prompt_security",
446-
"regex check input": "regex",
447-
"regex check output": "regex",
448-
"regex check retrieval": "regex",
449-
"self check facts": "self_check",
450-
"self check hallucination": "self_check",
451-
"self check input": "self_check",
452-
"self check output": "self_check",
453-
"topic safety check input": "topic_safety",
454-
"trend ai guard input": "trend_micro",
455-
"trend ai guard output": "trend_micro",
456-
}
457-
458-
_CONFIG_BUILTIN_FEATURE_ALIASES = {
390+
_BUILTIN_FEATURE_ID_ALIASES = {
459391
"fact_checking": "factchecking",
460392
"patronus": "patronusai",
393+
"privateai": "sensitive_data_detection",
461394
"regex_detection": "regex",
462395
}
463396

397+
_BUILTIN_FLOW_FEATURE_OVERRIDES = {
398+
"self check hallucination": "self_check",
399+
}
400+
464401
_COLANG_V2_LIBRARY_DIR = Path(__file__).resolve().parent / "colang" / "v2_x" / "library"
465402

466403

467-
def _normalize_builtin_feature_id(field_name: str) -> str:
468-
"""Return the documented feature id for a RailsConfigData field."""
469-
return _CONFIG_BUILTIN_FEATURE_ALIASES.get(field_name, field_name)
404+
def _normalize_builtin_feature_id(feature_id: str) -> str:
405+
"""Return the stable telemetry id for a config field or manifest."""
406+
return _BUILTIN_FEATURE_ID_ALIASES.get(feature_id, feature_id.split(".", 1)[0])
470407

471408

472409
def _flow_file_name(flow: Any) -> Optional[str]:
@@ -509,7 +446,7 @@ def _detect_builtin_features(config: "RailsConfig") -> List[str]:
509446
510447
Uses two signals: (1) fields on ``RailsConfigData`` that differ from
511448
their defaults (explicit config), and (2) exact-match flow names
512-
against a known set of built-in library flows. Only our own feature
449+
declared by the built-in rail manifest catalog. Only our own feature
513450
names are ever reported, never user-defined flow names.
514451
515452
Args:
@@ -539,6 +476,17 @@ def _detect_builtin_features(config: "RailsConfig") -> List[str]:
539476
except Exception:
540477
pass
541478

479+
try:
480+
from nemoguardrails.manifests import default_rail_catalog
481+
482+
catalog = default_rail_catalog()
483+
except Exception:
484+
log.debug("Failed to load the built-in rail catalog for usage telemetry", exc_info=True)
485+
catalog = None
486+
487+
if catalog is None:
488+
return sorted(features)
489+
542490
all_flows = []
543491
for rail_group in ["input", "output", "retrieval", "tool_output", "tool_input"]:
544492
group = getattr(rails, rail_group, None)
@@ -547,9 +495,14 @@ def _detect_builtin_features(config: "RailsConfig") -> List[str]:
547495

548496
for flow_name in all_flows:
549497
normalized = _normalize_flow_id(flow_name)
550-
feature = _KNOWN_BUILTIN_FLOWS.get(normalized)
551-
if feature is not None:
552-
features.add(feature)
498+
manifest_name = catalog.owner_for_flow(normalized)
499+
if manifest_name is None:
500+
continue
501+
feature = _BUILTIN_FLOW_FEATURE_OVERRIDES.get(
502+
normalized,
503+
_normalize_builtin_feature_id(manifest_name),
504+
)
505+
features.add(feature)
553506

554507
return sorted(features)
555508

tests/telemetry/test_usage_reporting.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
_detect_builtin_features,
3232
_get_heartbeat_interval_s,
3333
_is_usage_stats_enabled,
34+
_normalize_builtin_feature_id,
3435
_rotate_audit_file,
3536
_send_report,
3637
_write_audit_file,
@@ -983,6 +984,32 @@ def test_config_feature_ids_are_normalized(self):
983984
assert "patronus" not in result
984985
assert "regex_detection" not in result
985986

987+
@pytest.mark.parametrize(
988+
("feature_id", "expected"),
989+
[
990+
("factchecking.align_score", "factchecking"),
991+
("self_check.tool_call", "self_check"),
992+
],
993+
)
994+
def test_dotted_feature_ids_use_namespace(self, feature_id, expected):
995+
assert _normalize_builtin_feature_id(feature_id) == expected
996+
997+
def test_documented_builtin_feature_ids_match_manifest_catalog(self):
998+
from nemoguardrails.manifests import default_rail_catalog
999+
1000+
docs = (Path(__file__).parents[2] / "docs" / "telemetry.mdx").read_text(encoding="utf-8")
1001+
section = docs.split("### Possible Values for Built-In Features", 1)[1].split(
1002+
"## Data Not Collected by Telemetry",
1003+
1,
1004+
)[0]
1005+
documented_line = next(line for line in section.splitlines() if line.startswith("`"))
1006+
documented = set(documented_line.removesuffix(".").replace("`", "").split(", "))
1007+
expected = {
1008+
_normalize_builtin_feature_id(manifest.name) for manifest in default_rail_catalog().manifests.values()
1009+
}
1010+
1011+
assert documented == expected
1012+
9861013
def test_detects_features_from_exact_flow_names(self):
9871014
from nemoguardrails.rails.llm.config import Rails
9881015

@@ -999,6 +1026,107 @@ def test_detects_features_from_exact_flow_names(self):
9991026
assert "topic_safety" in result
10001027
assert "jailbreak_detection" in result
10011028

1029+
def test_detects_features_added_to_manifest_catalog(self):
1030+
from nemoguardrails.rails.llm.config import Rails
1031+
1032+
config = MagicMock()
1033+
config.rails = Rails()
1034+
config.rails.input.flows = [
1035+
"context bloat detection on input",
1036+
"f5 guardrails scan input",
1037+
"gcpnlp moderation",
1038+
"hf classifier check input",
1039+
"polygraf detect pii on input",
1040+
]
1041+
1042+
result = _detect_builtin_features(config)
1043+
1044+
assert result == [
1045+
"context_bloat_detection",
1046+
"f5",
1047+
"gcp_moderate_text",
1048+
"hf_classifier",
1049+
"polygraf",
1050+
]
1051+
1052+
def test_manifest_catalog_failure_skips_flow_detection(self):
1053+
from nemoguardrails.rails.llm.config import JailbreakDetectionConfig, Rails, RailsConfigData
1054+
1055+
config = MagicMock()
1056+
config.rails = Rails(
1057+
config=RailsConfigData(
1058+
jailbreak_detection=JailbreakDetectionConfig(nim_base_url="https://example.com"),
1059+
)
1060+
)
1061+
config.rails.input.flows = ["content safety check input"]
1062+
1063+
with patch(
1064+
"nemoguardrails.manifests.default_rail_catalog",
1065+
side_effect=RuntimeError("catalog unavailable"),
1066+
):
1067+
assert _detect_builtin_features(config) == ["jailbreak_detection"]
1068+
1069+
def test_every_manifest_flow_resolves_to_the_expected_builtin_feature(self):
1070+
from nemoguardrails.manifests import default_rail_catalog
1071+
from nemoguardrails.rails.llm.config import Rails
1072+
1073+
config = MagicMock()
1074+
config.rails = Rails()
1075+
catalog = default_rail_catalog()
1076+
manifest_feature_ids = {
1077+
"privateai": "sensitive_data_detection",
1078+
}
1079+
flow_feature_overrides = {
1080+
"self check hallucination": "self_check",
1081+
}
1082+
1083+
mismatches = []
1084+
for manifest in catalog.manifests.values():
1085+
if manifest.flows is None:
1086+
continue
1087+
for flow_name in manifest.flows.flow_names:
1088+
config.rails.input.flows = [flow_name]
1089+
expected = flow_feature_overrides.get(
1090+
flow_name,
1091+
manifest_feature_ids.get(manifest.name, manifest.name.split(".", 1)[0]),
1092+
)
1093+
actual = _detect_builtin_features(config)
1094+
if actual != [expected]:
1095+
mismatches.append((flow_name, expected, actual))
1096+
1097+
assert mismatches == []
1098+
1099+
@pytest.mark.parametrize(
1100+
("flow_name", "feature_id"),
1101+
[
1102+
("alignscore check facts", "factchecking"),
1103+
("detect pii on input", "sensitive_data_detection"),
1104+
("self check facts", "self_check"),
1105+
("self check hallucination", "self_check"),
1106+
],
1107+
)
1108+
def test_manifest_flow_preserves_compatibility_feature_id(self, flow_name, feature_id):
1109+
from nemoguardrails.rails.llm.config import Rails
1110+
1111+
config = MagicMock()
1112+
config.rails = Rails()
1113+
config.rails.input.flows = [flow_name]
1114+
1115+
assert _detect_builtin_features(config) == [feature_id]
1116+
1117+
def test_privateai_config_and_flow_share_compatibility_feature_id(self):
1118+
from nemoguardrails.rails.llm.config import PrivateAIDetection, Rails, RailsConfigData
1119+
1120+
config = MagicMock()
1121+
config.rails = Rails(
1122+
config=RailsConfigData(
1123+
privateai=PrivateAIDetection(server_endpoint="https://private-ai.example.com"),
1124+
)
1125+
)
1126+
config.rails.input.flows = ["detect pii on input"]
1127+
1128+
assert _detect_builtin_features(config) == ["sensitive_data_detection"]
1129+
10021130
def test_ignores_unknown_flow_names(self):
10031131
from nemoguardrails.rails.llm.config import Rails
10041132

0 commit comments

Comments
 (0)