trulens-apps-rl 2.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: trulens-apps-rl
3
+ Version: 2.12.0
4
+ Summary: RL reward integration for TruLens feedback metrics.
5
+ License: MIT
6
+ Author: Snowflake Inc.
7
+ Author-email: ml-observability-wg-dl@snowflake.com
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Requires-Dist: trulens-core (>=2.0.0,<3.0.0)
20
+ Project-URL: Documentation, https://trulens.org/getting_started/
21
+ Project-URL: Homepage, https://trulens.org/
22
+ Project-URL: Repository, https://github.com/truera/trulens
23
+ Description-Content-Type: text/markdown
24
+
25
+ # TruLens Apps - RL
26
+
27
+ Reinforcement Learning (RL) reward integration adapter for TruLens feedback metrics.
28
+
@@ -0,0 +1,3 @@
1
+ # TruLens Apps - RL
2
+
3
+ Reinforcement Learning (RL) reward integration adapter for TruLens feedback metrics.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ build-backend = "poetry.core.masonry.api"
3
+ requires = [
4
+ "poetry-core",
5
+ ]
6
+
7
+ [tool.poetry]
8
+ name = "trulens-apps-rl"
9
+ version = "2.12.0"
10
+ description = "RL reward integration for TruLens feedback metrics."
11
+ authors = [
12
+ "Snowflake Inc. <ml-observability-wg-dl@snowflake.com>",
13
+ ]
14
+ license = "MIT"
15
+ readme = "README.md"
16
+ packages = [
17
+ { include = "trulens" },
18
+ ]
19
+ homepage = "https://trulens.org/"
20
+ documentation = "https://trulens.org/getting_started/"
21
+ repository = "https://github.com/truera/trulens"
22
+ classifiers = [
23
+ "Programming Language :: Python :: 3",
24
+ "Operating System :: OS Independent",
25
+ "Development Status :: 3 - Alpha",
26
+ "License :: OSI Approved :: MIT License",
27
+ ]
28
+
29
+ [tool.poetry.dependencies]
30
+ python = "^3.9"
31
+ trulens-core = { version = "^2.0.0" }
32
+
33
+ [tool.poetry.group.dev.dependencies]
34
+ trulens-core = { path = "../../core" }
@@ -0,0 +1,22 @@
1
+ """
2
+ !!! note "Additional Dependency Required"
3
+
4
+ To use this module, you must have the `trulens-apps-rl` package installed.
5
+
6
+ ```bash
7
+ pip install trulens-apps-rl
8
+ ```
9
+ """
10
+
11
+ from importlib.metadata import version
12
+
13
+ from trulens.apps.rl.reward import RewardFunction
14
+ from trulens.apps.rl.reward import TRLRewardAdapter
15
+ from trulens.core.utils.imports import safe_importlib_package_name
16
+
17
+ __version__ = version(safe_importlib_package_name(__package__ or __name__))
18
+
19
+ __all__ = [
20
+ "RewardFunction",
21
+ "TRLRewardAdapter",
22
+ ]
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561
@@ -0,0 +1,291 @@
1
+ """Reinforcement Learning (RL) reward function adapter for TruLens feedback functions.
2
+
3
+ Provides:
4
+ - :class:`RewardFunction`: Wraps any TruLens feedback function into a scalar reward function
5
+ suitable for RL fine-tuning (e.g. Hugging Face TRL PPOTrainer / GRPOTrainer).
6
+ - :class:`TRLRewardAdapter`: High-level adapter specifically designed for TRL reward_funcs signature.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ import inspect
13
+ import logging
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def transform_2x_minus_1(score: float) -> float:
20
+ """Transform score in [0, 1] to reward signal in [-1, 1]."""
21
+ return 2.0 * float(score) - 1.0
22
+
23
+
24
+ def transform_identity(score: float) -> float:
25
+ """Identity transform (returns score unchanged in [0, 1])."""
26
+ return float(score)
27
+
28
+
29
+ class RewardFunction:
30
+ """Adapts a TruLens feedback function into an RL reward signal.
31
+
32
+ Score Transformation Guidance
33
+ -----------------------------
34
+ Selection of the ``transform`` parameter depends on your RL algorithm:
35
+
36
+ - ``"2x-1"`` (default): Maps $[0, 1]$ feedback scores to $[-1, 1]$ reward signals.
37
+ Recommended for policy gradient methods like **PPO** and **GRPO** that expect
38
+ symmetric positive/negative reward signals centered at zero (positive rewards
39
+ reinforce high-quality completions, negative rewards penalize poor ones).
40
+ - ``"identity"``: Preserves original $[0, 1]$ scores unchanged. Recommended when
41
+ training a **Reward Model**, or when using RL trainers that perform internal
42
+ z-score reward normalization (e.g. TRL GRPOTrainer with reward whitening).
43
+ - Custom callable ``(float) -> float``: For custom reward shaping curves.
44
+
45
+ Parameters
46
+ ----------
47
+ feedback_fn:
48
+ A TruLens feedback callable returning float or (float, dict).
49
+ transform:
50
+ Optional function or string name ("2x-1", "identity") to transform [0, 1]
51
+ scores into RL scalar rewards. Defaults to "2x-1".
52
+ app_name:
53
+ Optional virtual app name for TruLens trajectory logging.
54
+ app_version:
55
+ Optional virtual app version for TruLens trajectory logging.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ feedback_fn: Callable[..., Any],
61
+ *,
62
+ transform: str | Callable[[float], float] = "2x-1",
63
+ app_name: str | None = None,
64
+ app_version: str | None = None,
65
+ ) -> None:
66
+ self.feedback_fn = feedback_fn
67
+
68
+ if isinstance(transform, str):
69
+ if transform in ("2x-1", "2*score - 1", "2*s-1"):
70
+ self._transform_fn = transform_2x_minus_1
71
+ elif transform in ("identity", "none", "passthrough"):
72
+ self._transform_fn = transform_identity
73
+ else:
74
+ raise ValueError(
75
+ f"Unknown transform string '{transform}'. Use '2x-1', 'identity', or pass a custom callable."
76
+ )
77
+ elif callable(transform):
78
+ self._transform_fn = transform
79
+ else:
80
+ self._transform_fn = transform_identity
81
+
82
+ # Inspect feedback_fn signature once at initialization to avoid bare except catches during training
83
+ self._inspect_signature()
84
+
85
+ self._recorder: Any | None = None
86
+ if (app_name is None) != (app_version is None):
87
+ raise ValueError(
88
+ "Must supply both app_name and app_version to enable logging, or neither."
89
+ )
90
+ if app_name is not None and app_version is not None:
91
+ from trulens.apps.virtual import TruVirtual
92
+
93
+ self._recorder = TruVirtual(
94
+ app_name=app_name, app_version=app_version
95
+ )
96
+
97
+ def _inspect_signature(self) -> None:
98
+ """Inspect feedback_fn signature once to set dispatch mode for fast, debuggable execution."""
99
+ try:
100
+ sig = inspect.signature(self.feedback_fn)
101
+ params = list(sig.parameters.keys())
102
+ has_var_kwargs = any(
103
+ p.kind == inspect.Parameter.VAR_KEYWORD
104
+ for p in sig.parameters.values()
105
+ )
106
+
107
+ if "prompt" in params and "response" in params:
108
+ self._call_mode = "prompt_response"
109
+ elif "prompt" in params and "completion" in params:
110
+ self._call_mode = "prompt_completion"
111
+ elif "input" in params and "output" in params:
112
+ self._call_mode = "input_output"
113
+ elif "text" in params:
114
+ self._call_mode = "text"
115
+ elif has_var_kwargs or len(params) >= 2:
116
+ self._call_mode = "prompt_response"
117
+ elif len(params) == 1:
118
+ self._call_mode = "positional_1"
119
+ else:
120
+ self._call_mode = "positional_2"
121
+ except (ValueError, TypeError):
122
+ self._call_mode = "prompt_response"
123
+
124
+ @classmethod
125
+ def from_metric(
126
+ cls,
127
+ metric: Any,
128
+ *,
129
+ transform: str | Callable[[float], float] = "2x-1",
130
+ app_name: str | None = None,
131
+ app_version: str | None = None,
132
+ ) -> RewardFunction:
133
+ """Create a RewardFunction directly from a TruLens Metric object or callable.
134
+
135
+ In TruLens, a :class:`~trulens.core.Metric` encapsulates an evaluation metric's
136
+ implementation (e.g. ``provider.relevance`` or ``provider.groundedness``), its
137
+ selectors, and configuration.
138
+
139
+ Example
140
+ -------
141
+ ::
142
+
143
+ from trulens.apps.rl import RewardFunction
144
+ from trulens.core import Metric
145
+ from trulens.providers.openai import OpenAI
146
+
147
+ provider = OpenAI()
148
+ metric = Metric(
149
+ implementation=provider.relevance,
150
+ name="Relevance",
151
+ )
152
+ reward_fn = RewardFunction.from_metric(metric, transform="2x-1")
153
+ """
154
+ if hasattr(metric, "implementation") and callable(
155
+ metric.implementation
156
+ ):
157
+ feedback_fn = metric.implementation
158
+ elif callable(metric):
159
+ feedback_fn = metric
160
+ else:
161
+ raise ValueError(
162
+ f"Cannot extract callable implementation from metric: {metric}"
163
+ )
164
+
165
+ return cls(
166
+ feedback_fn=feedback_fn,
167
+ transform=transform,
168
+ app_name=app_name,
169
+ app_version=app_version,
170
+ )
171
+
172
+ def evaluate_sample(
173
+ self, prompt: str, completion: str, **kwargs: Any
174
+ ) -> float:
175
+ """Evaluate a single (prompt, completion) pair and return its scalar reward."""
176
+ if self._call_mode == "prompt_response":
177
+ raw_result = self.feedback_fn(
178
+ prompt=prompt, response=completion, **kwargs
179
+ )
180
+ elif self._call_mode == "prompt_completion":
181
+ raw_result = self.feedback_fn(
182
+ prompt=prompt, completion=completion, **kwargs
183
+ )
184
+ elif self._call_mode == "input_output":
185
+ raw_result = self.feedback_fn(
186
+ input=prompt, output=completion, **kwargs
187
+ )
188
+ elif self._call_mode == "text":
189
+ raw_result = self.feedback_fn(
190
+ text=f"{prompt}\n{completion}", **kwargs
191
+ )
192
+ elif self._call_mode == "positional_1":
193
+ raw_result = self.feedback_fn(prompt, **kwargs)
194
+ else:
195
+ raw_result = self.feedback_fn(prompt, completion, **kwargs)
196
+
197
+ score = (
198
+ float(raw_result[0])
199
+ if isinstance(raw_result, tuple)
200
+ else float(raw_result)
201
+ )
202
+ reward = self._transform_fn(score)
203
+
204
+ if self._recorder is not None:
205
+ self._log_reward(
206
+ prompt=prompt,
207
+ completion=completion,
208
+ score=score,
209
+ reward=reward,
210
+ )
211
+
212
+ return reward
213
+
214
+ def __call__(
215
+ self,
216
+ prompts: list[str],
217
+ completions: list[str],
218
+ **kwargs: Any,
219
+ ) -> list[float]:
220
+ """Evaluate a batch of prompts and completions, returning a list of float rewards."""
221
+ if len(prompts) != len(completions):
222
+ raise ValueError(
223
+ f"Mismatched batch size: {len(prompts)} prompts vs {len(completions)} completions."
224
+ )
225
+
226
+ rewards: list[float] = []
227
+ for prompt, completion in zip(prompts, completions):
228
+ reward = self.evaluate_sample(prompt, completion, **kwargs)
229
+ rewards.append(reward)
230
+
231
+ return rewards
232
+
233
+ def _log_reward(
234
+ self, prompt: str, completion: str, score: float, reward: float
235
+ ) -> None:
236
+ """Log reward evaluation trajectory."""
237
+ try:
238
+ from trulens.apps.virtual import VirtualRecord
239
+ from trulens.core import Select
240
+
241
+ call_selector = Select.RecordCalls.reward_fn.evaluate
242
+ record = VirtualRecord(
243
+ main_input=f"Prompt: {prompt} | Completion: {completion}",
244
+ main_output=str(reward),
245
+ calls={
246
+ call_selector: {
247
+ "args": [prompt, completion],
248
+ "rets": {"score": score, "reward": reward},
249
+ }
250
+ },
251
+ )
252
+ self._recorder.add_record(record)
253
+ except Exception as exc: # noqa: BLE001
254
+ logger.warning(
255
+ "Failed to log RL reward evaluation to TruLens: %s", exc
256
+ )
257
+
258
+
259
+ class TRLRewardAdapter(RewardFunction):
260
+ """TRL (Transformer Reinforcement Learning) adapter wrapping TruLens metrics as TRL reward_funcs.
261
+
262
+ Supported TRL Trainers & Versions
263
+ --------------------------------
264
+ Tested and compatible with **Hugging Face TRL >= 0.7.0** (including **0.12.0+**
265
+ `GRPOTrainer` and `PPOTrainer`).
266
+
267
+ TRL trainers pass decoded prompt text strings (``prompts: list[str]``) and
268
+ completion text strings (``completions: list[str]``) to reward functions in
269
+ the signature ``reward_func(prompts, completions, **kwargs) -> list[float]``.
270
+
271
+ Example with TRL GRPOTrainer
272
+ ----------------------------
273
+ ::
274
+
275
+ from trl import GRPOTrainer, GRPOConfig
276
+ from trulens.apps.rl import TRLRewardAdapter
277
+ from trulens.providers.openai import OpenAI
278
+
279
+ provider = OpenAI()
280
+ reward_adapter = TRLRewardAdapter(
281
+ feedback_fn=provider.relevance,
282
+ transform="2x-1",
283
+ )
284
+
285
+ trainer = GRPOTrainer(
286
+ model=model,
287
+ reward_funcs=[reward_adapter],
288
+ train_dataset=dataset,
289
+ args=GRPOConfig(output_dir="./results"),
290
+ )
291
+ """