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,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
|
+
"""
|