Source code for slicktune.rewards

"""Verifiable reward helpers for GRPO / RL objectives."""

from __future__ import annotations

import re
from typing import Any

_STOPWORDS = frozenset(
    {
        "a",
        "an",
        "the",
        "of",
        "to",
        "in",
        "on",
        "for",
        "and",
        "or",
        "is",
        "are",
        "be",
        "yes",
        "no",
        "uses",
    }
)


def _completion_text(completion: Any) -> str:
    """Flatten a GRPO completion to plain text.

    Parameters
    ----------
    completion : Any
        Either a string or a chat message list (TRL conversational format).

    Returns
    -------
    str
        Extracted completion text.
    """
    if isinstance(completion, str):
        return completion
    if isinstance(completion, list):
        parts: list[str] = []
        for turn in completion:
            if isinstance(turn, dict) and "content" in turn:
                parts.append(str(turn["content"]))
            else:
                parts.append(str(turn))
        return "\n".join(parts)
    return str(completion)


def _keywords(needle: str) -> list[str]:
    """Extract content keywords from a ``must_contain`` needle.

    Parameters
    ----------
    needle : str
        Required substring / phrase.

    Returns
    -------
    list[str]
        Lowercased content tokens used for soft overlap rewards.
    """
    tokens = re.findall(r"[a-z0-9@./_+-]+", needle.lower())
    return [token for token in tokens if token not in _STOPWORDS and len(token) > 2]


[docs] def substring_must_contain_reward( completions: list[Any], must_contain: list[str], **kwargs: Any, ) -> list[float]: """Score completions against required substrings. Full case-insensitive substring match → ``1.0``. Otherwise return the fraction of content keywords from ``must_contain`` that appear in the completion (denser signal for GRPO when the base model rarely emits the exact phrase). Parameters ---------- completions : list Generated completions (strings or chat message lists). must_contain : list[str] Required substrings aligned with ``completions``. **kwargs : Any Unused TRL extras (``prompts``, ``trainer_state``, …). Returns ------- list[float] Per-completion rewards in ``[0.0, 1.0]``. """ del kwargs rewards: list[float] = [] for completion, needle in zip(completions, must_contain, strict=True): text = _completion_text(completion) lowered = text.lower() if needle.lower() in lowered: rewards.append(1.0) continue keys = _keywords(needle) if not keys: rewards.append(0.0) continue hits = sum(1 for key in keys if key in lowered) rewards.append(hits / len(keys)) return rewards
__all__ = ["substring_must_contain_reward"]