Skip to content

fix: coerce low combined_risk_level to medium instead of rejecting - #210

Open
asteier2026 wants to merge 5 commits into
mainfrom
asteier2026/bugfix/sens-disp-low-risk-coercion
Open

fix: coerce low combined_risk_level to medium instead of rejecting#210
asteier2026 wants to merge 5 commits into
mainfrom
asteier2026/bugfix/sens-disp-low-risk-coercion

Conversation

@asteier2026

@asteier2026 asteier2026 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The LLM occasionally outputs combined_risk_level='low' with a non-leave_as_is
    protection_method_suggestion (e.g. suppress_inference, generalize)
  • Previously EntityDispositionSchema._validate_protection_consistency raised a ValidationError, which
    caused the entire record to be skipped (UPDATE - sometimes the record isn't skipped, but the entity silently gets dropped from the sensitivity disposition)
  • Now combined_risk_level is promoted to medium — preserving the model's intent to protect the entity
    rather than suppressing it. This is more appropriate for weaker models that apply protection methods
    without fully accounting for contextual risk
  • Updated the corresponding test from asserting a ValidationError to asserting that
    combined_risk_level is promoted and the original protection_method_suggestion is retained

UPDATE - the sensitivity disposition prompt is overloaded and doesn't do as well on gpt-oss-120b. We will do another PR to fix that, but above is a quick fix for now

@asteier2026
asteier2026 requested a review from a team as a code owner July 2, 2026 17:30
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes how EntityDispositionSchema handles the inconsistent case where an LLM returns combined_risk_level='low' alongside a protective (non-leave_as_is) method. Instead of raising a ValidationError (which could silently drop entities from the sensitivity disposition), the validator now logs a warning and promotes combined_risk_level to medium, preserving the protection intent.

  • rewrite.py: The validator coercion is safe — CombinedRiskLevel is a str, Enum, so the in-validator assignment of CombinedRiskLevel.medium is equivalent to the string "medium" throughout serialization and downstream comparisons; use_enum_values=True poses no issue here.
  • test_schemas.py: The parametrized test now covers all four active protection methods (replace, generalize, remove, suppress_inference) and asserts both that the risk level is promoted and that the original method is retained. All three previous reviewer concerns (logging, test coverage, and description–code alignment) have been addressed.

Confidence Score: 5/5

Safe to merge — the coercion replaces a silent entity-drop with an explicit risk-level promotion, the warning log is in place, and the tests cover all four protection methods.

The change is narrowly scoped to one validator branch. The coercion is semantically sound, the warning gives operators an observable signal, and the parametrized test catches regressions across every relevant protection method. No downstream consumers of combined_risk_level are broken by the promotion from low to medium.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/anonymizer/engine/schemas/rewrite.py Replaces the ValidationError raised for low-risk + non-leave_as_is combos with a warning log and coercion of combined_risk_level to medium; logic is correct and consistent with model semantics.
tests/engine/test_schemas.py Old 'expects ValidationError' test replaced with a parametrized test covering all four coercible protection methods; both the promoted risk level and the retained protection method are asserted.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["LLM output: combined_risk_level='low'\nprotection_method_suggestion=X"] --> B{"X == leave_as_is?"}
    B -- Yes --> C["Valid: no coercion needed"]
    B -- No --> D["Log warning\n(Entity id, label, method)"]
    D --> E["Promote combined_risk_level → 'medium'\nKeep protection_method_suggestion=X"]
    E --> F{"combined_risk_level == 'high'\nAND method == 'leave_as_is'?"}
    C --> F
    F -- Yes --> G["Raise ValidationError"]
    F -- No --> H["Return validated EntityDispositionSchema\n(protection intent preserved)"]
Loading

Reviews (5): Last reviewed commit: "fix: redact entity_value from coercion w..." | Re-trigger Greptile

Comment thread src/anonymizer/engine/schemas/rewrite.py Outdated
Comment thread tests/engine/test_schemas.py Outdated
Comment thread src/anonymizer/engine/schemas/rewrite.py
@asteier2026 asteier2026 changed the title bugfix: coerce low combined_risk_level to leave_as_is instead of rejecting bugfix: coerce low combined_risk_level to medium instead of rejecting Jul 2, 2026
Comment thread src/anonymizer/engine/schemas/rewrite.py Outdated
@asteier2026 asteier2026 changed the title bugfix: coerce low combined_risk_level to medium instead of rejecting fix: coerce low combined_risk_level to medium instead of rejecting Jul 29, 2026
asteier2026 and others added 5 commits July 29, 2026 11:29
…cting

The LLM occasionally outputs combined_risk_level='low' with a non-leave_as_is
protection_method_suggestion. Previously this caused a ValidationError that
dropped the entire record. Now the protection_method_suggestion is silently
coerced to leave_as_is, consistent with the semantics of low combined risk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: asteier2026 <asteier@nvidia.com>
…protection

When the LLM assigns combined_risk_level='low' alongside a non-leave_as_is
protection method, trust the protection intent over the risk label. Promoting
the risk level to medium preserves the model's decision to protect the entity
rather than silently discarding it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: asteier2026 <asteier@nvidia.com>
…nsistency

Silent coercion made it impossible to detect in production whether the
inconsistency was a rare blip or a systematic regression from a prompt
or model change. The warning names the entity and both conflicting field
values so it is actionable in logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: asteier2026 <asteier@nvidia.com>
…hods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: asteier2026 <asteier@nvidia.com>
Replace entity_value with entity_label in the logger.warning call so
raw PII strings are not written to log aggregators.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@asteier2026
asteier2026 force-pushed the asteier2026/bugfix/sens-disp-low-risk-coercion branch from 0fb61f0 to e5a4b05 Compare July 29, 2026 18:29

@lipikaramaswamy lipikaramaswamy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One non-blocking serialization consistency suggestion.

)
# Trust the protection intent over the risk label; promote risk to medium
# rather than suppressing the protection.
self.combined_risk_level = CombinedRiskLevel.medium

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: because model_config uses use_enum_values=True, normally validated combined_risk_level values are stored and returned by model_dump() as plain strings. This assignment happens after validation, so the coerced path instead stores a CombinedRiskLevel enum object (although it compares equal to "medium"). Could we assign CombinedRiskLevel.medium.value here and add a model_dump() assertion so coerced and non-coerced records preserve the same serialization contract?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made suggested changs.

@asteier2026

Copy link
Copy Markdown
Contributor Author

Note that this branch has a different version of rewrite_generation.py than what is being updated in PR 208 - it doesn't have the prereplace logic. How do we handle that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants