Source code for oumi.datasets.grpo.rewards.rar_medicine_reward

# Copyright 2025 - Oumi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""LLM-judge reward for the RaR-Medicine dataset."""

import logging
import os
import random
import threading
import time
from typing import TYPE_CHECKING, Any

from oumi.core.registry import RegistryType, register

if TYPE_CHECKING:
    from oumi.judges.simple_judge import SimpleJudge

logger = logging.getLogger(__name__)

_DEFAULT_JUDGE_CONFIG = "configs/examples/grpo_verl_medqa/judge.yaml"
_MAX_CONCURRENCY = int(os.environ.get("RAR_JUDGE_MAX_CONCURRENCY", "16"))
_MAX_ATTEMPTS = int(os.environ.get("RAR_JUDGE_MAX_ATTEMPTS", "2"))

_judge: "SimpleJudge | None" = None
_judge_init_lock = threading.Lock()
_judge_semaphore = threading.BoundedSemaphore(_MAX_CONCURRENCY)


def _get_judge(judge_config_path: str) -> "SimpleJudge":
    """Build and return the process-wide judge."""
    global _judge
    with _judge_init_lock:
        if _judge is None:
            if not os.environ.get("OPENAI_API_KEY"):
                raise RuntimeError(
                    "OPENAI_API_KEY must be set to use the RaR-Medicine reward."
                )
            from oumi.judges.simple_judge import SimpleJudge

            _judge = SimpleJudge(judge_config_path)
    return _judge


def score_rar_medicine_response(
    question: str,
    reference_answer: str,
    response: str,
    judge_config_path: str = _DEFAULT_JUDGE_CONFIG,
) -> float:
    """Return the medical judge's integer score normalized to ``[0, 1]``."""
    judge_config_path = os.environ.get("RAR_JUDGE_CONFIG", judge_config_path)
    judge_input = {
        "question": question,
        "reference_answer": reference_answer,
        "response": response,
    }

    last_error: Exception | None = None
    for attempt in range(_MAX_ATTEMPTS):
        try:
            with _judge_semaphore:
                outputs = _get_judge(judge_config_path).judge([judge_input])
            judgment = outputs[0].field_values.get("judgment")
            if judgment is None:
                raise ValueError("The judge returned no parseable judgment.")
            return min(max(float(judgment), 0.0), 10.0) / 10.0
        except Exception as error:
            last_error = error
            if attempt + 1 < _MAX_ATTEMPTS:
                time.sleep(2**attempt + random.random())

    logger.warning(
        "RaR-Medicine judge failed after %d attempts (%s); assigning reward 0.",
        _MAX_ATTEMPTS,
        last_error,
    )
    return 0.0


[docs] @register("rar_medicine_verl", RegistryType.REWARD_FUNCTION) def rar_medicine_verl( data_source: str, solution_str: str, ground_truth: str, extra_info: dict[str, Any] | None = None, judge_config_path: str = _DEFAULT_JUDGE_CONFIG, **kwargs: Any, ) -> float: """Score a VERL completion against its RaR-Medicine reference answer.""" del data_source, kwargs if not solution_str or not solution_str.strip(): return 0.0 question = str((extra_info or {}).get("question", "")) return score_rar_medicine_response( question, ground_truth, solution_str, judge_config_path )