Skip to content

MAINT Extract LLM-specific scoring concerns out of the Scorer base class #2143

Description

@immu4989

Following up on the note @romanlutz left in #1867: the Scorer base class carries machinery that only LLM-backed scorers use, and it would be cleaner to separate that out. Opening this to propose a concrete shape and get direction before writing code.

The problem

Scorer currently owns several members that are only meaningful when the scorer calls an LLM:

  • _score_value_with_llm_async(...) (the JSON/parser plumbing, retries, system-prompt wiring)
  • the chat_target parameter on __init__ and the TARGET_REQUIREMENTS validation it triggers
  • get_chat_target() and the _prompt_target attribute convention
  • the prompt_target promotion inside _create_identifier

About half of the current scorers never use any of this. The regex family (RegexScorer, CredentialLeakScorer, PathTraversalOutputScorer, ShellCommandOutputScorer, SqlInjectionOutputScorer, XssOutputScorer) and the keyword family (AnthraxKeywordScorer, FentanylKeywordScorer, MethKeywordScorer, NerveAgentKeywordScorer), plus SubStringScorer and MarkdownInjectionScorer, are pure rule-based scorers. They inherit the LLM surface but never touch it.

The LLM-using scorers are the self_ask_* family and InsecureCodeScorer (11 in total). That split is stable and unlikely to change, so the concern is a good candidate for its own type.

Why a mixin rather than an intermediate base

The existing type hierarchy already discriminates on output shape: TrueFalseScorer vs FloatScaleScorer, and scorer_type keys off that via isinstance. "Uses an LLM" is orthogonal to "returns true/false vs float", so folding it into the linear hierarchy would force a combinatorial split (LLMTrueFalseScorer, LLMFloatScaleScorer, and so on).

A mixin composes cleanly instead:

class LLMScorerMixin:
    """Owns chat-target wiring and _score_value_with_llm_async for LLM-backed scorers."""
    ...

class SelfAskTrueFalseScorer(LLMScorerMixin, TrueFalseScorer):
    ...

class InsecureCodeScorer(LLMScorerMixin, FloatScaleScorer):
    ...

The base Scorer keeps only what every scorer needs: the validator, the score_async / _score_piece_async interface, memory access, identifier construction, and the batch/evaluate helpers.

Proposed phasing

To keep each PR reviewable and avoid a single large diff:

  1. Introduce LLMScorerMixin with the moved members, and have the LLM scorers inherit from it. Keep thin delegating shims on Scorer marked for 0.16.0 removal so nothing breaks for downstream code that subclassed Scorer directly and called _score_value_with_llm_async.
  2. Migrate the internal LLM scorers to the mixin and update their identifier construction.
  3. Remove the shims from Scorer once the deprecation window closes.

Each phase is a separate PR. Phase 1 is additive and backward-compatible.

Open questions

  1. Mixin versus intermediate base: I lean mixin for the orthogonality reason above. If you would rather keep a linear hierarchy, that changes the shape substantially, so worth settling first.
  2. Does TARGET_REQUIREMENTS and the chat_target validation move fully into the mixin, or does the base keep a no-op default so evaluate_async / batch helpers that read _prompt_target via getattr keep working unchanged?
  3. Naming: LLMScorerMixin, LLMScorer, or something else that fits PyRIT conventions.
  4. Deprecation-shim approach in Phase 1 versus a single atomic move. The shim path is safer for downstream subclasses but adds temporary surface. Do you have a preference given where 0.16.0 is in its cycle?

Happy to start on Phase 1 once the mixin versus base question is settled.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions