trulens-feedback 1.0.1__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,437 @@
1
+ Metadata-Version: 2.1
2
+ Name: trulens-feedback
3
+ Version: 1.0.1
4
+ Summary: A TruLens extension package implementing feedback functions for LLM App evaluation.
5
+ Home-page: https://trulens.org/
6
+ License: MIT
7
+ Author: Snowflake Inc.
8
+ Author-email: ml-observability-wg-dl@snowflake.com
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Dist: nltk (>=3.9.1,<4.0.0)
19
+ Requires-Dist: numpy (>=1.23)
20
+ Requires-Dist: pydantic (>=2,<3)
21
+ Requires-Dist: scikit-learn (>=1.3.0,<2.0.0)
22
+ Requires-Dist: scipy (>=1.10.1,<2.0.0)
23
+ Requires-Dist: trulens-core (>=1.0.0,<2.0.0)
24
+ Project-URL: Documentation, https://trulens.org/trulens/getting_started/
25
+ Project-URL: Repository, https://github.com/truera/trulens
26
+ Description-Content-Type: text/markdown
27
+
28
+ # trulens-feedback
29
+
30
+ ## Feedback Functions
31
+
32
+ The `Feedback` class contains the starting point for feedback function
33
+ specification and evaluation. A typical use-case looks like this:
34
+
35
+ ```python
36
+ from trulens.core import Feedback, Select, Feedback
37
+
38
+ hugs = feedback.Huggingface()
39
+
40
+ f_lang_match = Feedback(hugs.language_match)
41
+ .on_input_output()
42
+ ```
43
+
44
+ The components of this specifications are:
45
+
46
+ - **Provider classes** -- `feedback.OpenAI` contains feedback function
47
+ implementations like `context_relevance`. Other classes subtyping
48
+ `feedback.Provider` include `Huggingface` and `Cohere`.
49
+
50
+ - **Feedback implementations** -- `provider.context_relevance` is a feedback function
51
+ implementation. Feedback implementations are simple callables that can be run
52
+ on any arguments matching their signatures. In the example, the implementation
53
+ has the following signature:
54
+
55
+ ```python
56
+ def language_match(self, text1: str, text2: str) -> float:
57
+ ```
58
+
59
+ That is, `language_match` is a plain python method that accepts two pieces
60
+ of text, both strings, and produces a float (assumed to be between 0.0 and
61
+ 1.0).
62
+
63
+ - **Feedback constructor** -- The line `Feedback(provider.language_match)`
64
+ constructs a Feedback object with a feedback implementation.
65
+
66
+ - **Argument specification** -- The next line, `on_input_output`, specifies how
67
+ the `language_match` arguments are to be determined from an app record or app
68
+ definition. The general form of this specification is done using `on` but
69
+ several shorthands are provided. `on_input_output` states that the first two
70
+ argument to `language_match` (`text1` and `text2`) are to be the main app
71
+ input and the main output, respectively.
72
+
73
+ Several utility methods starting with `.on` provide shorthands:
74
+
75
+ - `on_input(arg) == on_prompt(arg: Optional[str])` -- both specify that the next
76
+ unspecified argument or `arg` should be the main app input.
77
+
78
+ - `on_output(arg) == on_response(arg: Optional[str])` -- specify that the next
79
+ argument or `arg` should be the main app output.
80
+
81
+ - `on_input_output() == on_input().on_output()` -- specifies that the first
82
+ two arguments of implementation should be the main app input and main app
83
+ output, respectively.
84
+
85
+ - `on_default()` -- depending on signature of implementation uses either
86
+ `on_output()` if it has a single argument, or `on_input_output` if it has
87
+ two arguments.
88
+
89
+ Some wrappers include additional shorthands:
90
+
91
+ ### llama_index-specific selectors
92
+
93
+ - `TruLlama.select_source_nodes()` -- outputs the selector for the source
94
+ documents part of the engine output.
95
+ - `TruLlama.select_context()` -- outputs the selector for the text of
96
+ the source documents part of the engine output.
97
+
98
+ ### langchain-specific selectors
99
+
100
+ - `Langchain.select_context()` -- outputs the selector for retrieved context
101
+ from the app's internal `get_relevant_documents` method.
102
+
103
+ ### NeMo-specific selectors
104
+
105
+ - `NeMo.select_context()` -- outputs the selector for the retrieved context
106
+ from the app's internal `search_relevant_chunks` method.
107
+
108
+
109
+ ## Fine-grained Selection and Aggregation
110
+
111
+ For more advanced control on the feedback function operation, we allow data
112
+ selection and aggregation. Consider this feedback example:
113
+
114
+ ```python
115
+ f_context_relevance = Feedback(openai.context_relevance)
116
+ .on_input()
117
+ .on(Select.Record.app.combine_docs_chain._call.args.inputs.input_documents[:].page_content)
118
+ .aggregate(numpy.min)
119
+
120
+ # Implementation signature:
121
+ # def context_relevance(self, question: str, statement: str) -> float:
122
+ ```
123
+
124
+ - **Argument Selection specification** -- Where we previously set,
125
+ `on_input_output` , the `on(Select...)` line enables specification of where
126
+ the statement argument to the implementation comes from. The form of the
127
+ specification will be discussed in further details in the Specifying Arguments
128
+ section.
129
+
130
+ - **Aggregation specification** -- The last line `aggregate(numpy.min)` specifies
131
+ how feedback outputs are to be aggregated. This only applies to cases where
132
+ the argument specification names more than one value for an input. The second
133
+ specification, for `statement` was of this type. The input to `aggregate` must
134
+ be a method which can be imported globally. This requirement is further
135
+ elaborated in the next section. This function is called on the `float` results
136
+ of feedback function evaluations to produce a single float. The default is
137
+ `numpy.mean`.
138
+
139
+ The result of these lines is that `f_context_relevance` can be now be run on
140
+ app/records and will automatically select the specified components of those
141
+ apps/records:
142
+
143
+ ```python
144
+ record: Record = ...
145
+ app: App = ...
146
+
147
+ feedback_result: FeedbackResult = f_context_relevance.run(app=app, record=record)
148
+ ```
149
+
150
+ The object can also be provided to an app wrapper for automatic evaluation:
151
+
152
+ ```python
153
+ app: App = TruChain(...., feedbacks=[f_context_relevance])
154
+ ```
155
+
156
+ ## Specifying Implementation Function and Aggregate
157
+
158
+ The function or method provided to the `Feedback` constructor is the
159
+ implementation of the feedback function which does the actual work of producing
160
+ a float indicating some quantity of interest.
161
+
162
+ **Note regarding FeedbackMode.DEFERRED** -- Any function or method (not static
163
+ or class methods presently supported) can be provided here but there are
164
+ additional requirements if your app uses the "deferred" feedback evaluation mode
165
+ (when `feedback_mode=FeedbackMode.DEFERRED` are specified to app constructor).
166
+ In those cases the callables must be functions or methods that are importable
167
+ (see the next section for details). The function/method performing the
168
+ aggregation has the same requirements.
169
+
170
+ ### Import requirement (DEFERRED feedback mode only)
171
+
172
+ If using deferred evaluation, the feedback function implementations and
173
+ aggregation implementations must be functions or methods from a Provider
174
+ subclass that is importable. That is, the callables must be accessible were you
175
+ to evaluate this code:
176
+
177
+ ```python
178
+ from somepackage.[...] import someproviderclass
179
+ from somepackage.[...] import somefunction
180
+
181
+ # [...] means optionally further package specifications
182
+
183
+ provider = someproviderclass(...) # constructor arguments can be included
184
+ feedback_implementation1 = provider.somemethod
185
+ feedback_implementation2 = somefunction
186
+ ```
187
+
188
+ For provided feedback functions, `somepackage` is `trulens.feedback` and
189
+ `someproviderclass` is `OpenAI` or one of the other `Provider` subclasses.
190
+ Custom feedback functions likewise need to be importable functions or methods of
191
+ a provider subclass that can be imported. Critically, functions or classes
192
+ defined locally in a notebook will not be importable this way.
193
+
194
+ ## Specifying Arguments
195
+
196
+ The mapping between app/records to feedback implementation arguments is
197
+ specified by the `on...` methods of the `Feedback` objects. The general form is:
198
+
199
+ ```python
200
+ feedback: Feedback = feedback.on(argname1=selector1, argname2=selector2, ...)
201
+ ```
202
+
203
+ That is, `Feedback.on(...)` returns a new `Feedback` object with additional
204
+ argument mappings, the source of `argname1` is `selector1` and so on for further
205
+ argument names. The types of `selector1` is `JSONPath` which we elaborate on in
206
+ the "Selector Details".
207
+
208
+ If argument names are omitted, they are taken from the feedback function
209
+ implementation signature in order. That is,
210
+
211
+ ```python
212
+ Feedback(...).on(argname1=selector1, argname2=selector2)
213
+ ```
214
+
215
+ and
216
+
217
+ ```python
218
+ Feedback(...).on(selector1, selector2)
219
+ ```
220
+
221
+ are equivalent assuming the feedback implementation has two arguments,
222
+ `argname1` and `argname2`, in that order.
223
+
224
+ ### Running Feedback
225
+
226
+ Feedback implementations are simple callables that can be run on any arguments
227
+ matching their signatures. However, once wrapped with `Feedback`, they are meant
228
+ to be run on outputs of app evaluation (the "Records"). Specifically,
229
+ `Feedback.run` has this definition:
230
+
231
+ ```python
232
+ def run(self,
233
+ app: Union[AppDefinition, JSON],
234
+ record: Record
235
+ ) -> FeedbackResult:
236
+ ```
237
+
238
+ That is, the context of a Feedback evaluation is an app (either as
239
+ `AppDefinition` or a JSON-like object) and a `Record` of the execution of the
240
+ aforementioned app. Both objects are indexable using "Selectors". By indexable
241
+ here we mean that their internal components can be specified by a Selector and
242
+ subsequently that internal component can be extracted using that selector.
243
+ Selectors for Feedback start by specifying whether they are indexing into an App
244
+ or a Record via the `__app__` and `__record__` special
245
+ attributes (see **Selectors** section below).
246
+
247
+ ### Selector Details
248
+
249
+ Selectors are of type `JSONPath` defined in `util.py` but are also aliased in
250
+ `schema.py` as `Select.Query`. Objects of this type specify paths into JSON-like
251
+ structures (enumerating `Record` or `App` contents).
252
+
253
+ By JSON-like structures we mean python objects that can be converted into JSON
254
+ or are base types. This includes:
255
+
256
+ - base types: strings, integers, dates, etc.
257
+
258
+ - sequences
259
+
260
+ - dictionaries with string keys
261
+
262
+ Additionally, JSONPath also index into general python objects like
263
+ `AppDefinition` or `Record` though each of these can be converted to JSON-like.
264
+
265
+ When used to index json-like objects, JSONPath are used as generators: the path
266
+ can be used to iterate over items from within the object:
267
+
268
+ ```python
269
+ class JSONPath...
270
+ ...
271
+ def __call__(self, obj: Any) -> Iterable[Any]:
272
+ ...
273
+ ```
274
+
275
+ In most cases, the generator produces only a single item but paths can also
276
+ address multiple items (as opposed to a single item containing multiple).
277
+
278
+ The syntax of this specification mirrors the syntax one would use with
279
+ instantiations of JSON-like objects. For every `obj` generated by `query: JSONPath`:
280
+
281
+ - `query[somekey]` generates the `somekey` element of `obj` assuming it is a
282
+ dictionary with key `somekey`.
283
+
284
+ - `query[someindex]` generates the index `someindex` of `obj` assuming it is
285
+ a sequence.
286
+
287
+ - `query[slice]` generates the **multiple** elements of `obj` assuming it is a
288
+ sequence. Slices include `:` or in general `startindex:endindex:step`.
289
+
290
+ - `query[somekey1, somekey2, ...]` generates **multiple** elements of `obj`
291
+ assuming `obj` is a dictionary and `somekey1`... are its keys.
292
+
293
+ - `query[someindex1, someindex2, ...]` generates **multiple** elements
294
+ indexed by `someindex1`... from a sequence `obj`.
295
+
296
+ - `query.someattr` depends on type of `obj`. If `obj` is a dictionary, then
297
+ `query.someattr` is an alias for `query[someattr]`. Otherwise if
298
+ `someattr` is an attribute of a python object `obj`, then `query.someattr`
299
+ generates the named attribute.
300
+
301
+ For feedback argument specification, the selectors should start with either
302
+ `__record__` or `__app__` indicating which of the two JSON-like structures to
303
+ select from (Records or Apps). `Select.Record` and `Select.App` are defined as
304
+ `Query().__record__` and `Query().__app__` and thus can stand in for the start of a
305
+ selector specification that wishes to select from a Record or App, respectively.
306
+ The full set of Query aliases are as follows:
307
+
308
+ - `Record = Query().__record__` -- points to the Record.
309
+
310
+ - App = Query().**app** -- points to the App.
311
+
312
+ - `RecordInput = Record.main_input` -- points to the main input part of a
313
+ Record. This is the first argument to the root method of an app (for
314
+ langchain Chains this is the `__call__` method).
315
+
316
+ - `RecordOutput = Record.main_output` -- points to the main output part of a
317
+ Record. This is the output of the root method of an app (i.e. `__call__`
318
+ for langchain Chains).
319
+
320
+ - `RecordCalls = Record.app` -- points to the root of the app-structured
321
+ mirror of calls in a record. See **App-organized Calls** Section above.
322
+
323
+ ## Multiple Inputs Per Argument
324
+
325
+ As in the `f_context_relevance` example, a selector for a _single_ argument may point
326
+ to more than one aspect of a record/app. These are specified using the slice or
327
+ lists in key/index positions. In that case, the feedback function is evaluated
328
+ multiple times, its outputs collected, and finally aggregated into a main
329
+ feedback result.
330
+
331
+ The collection of values for each argument of feedback implementation is
332
+ collected and every combination of argument-to-value mapping is evaluated with a
333
+ feedback definition. This may produce a large number of evaluations if more than
334
+ one argument names multiple values. In the dashboard, all individual invocations
335
+ of a feedback implementation are shown alongside the final aggregate result.
336
+
337
+ ## App/Record Organization (What can be selected)
338
+
339
+ Apps are serialized into JSON-like structures which are indexed via selectors.
340
+ The exact makeup of this structure is app-dependent though always start with
341
+ `app`, that is, the trulens wrappers (subtypes of `App`) contain the wrapped app
342
+ in the attribute `app`:
343
+
344
+ ```python
345
+ # app.py:
346
+ class App(AppDefinition, SerialModel):
347
+ ...
348
+ # The wrapped app.
349
+ app: Any = Field(exclude=True)
350
+ ...
351
+ ```
352
+
353
+ For your app, you can inspect the JSON-like structure by using the `dict`
354
+ method:
355
+
356
+ ```python
357
+ app = ... # your app, extending App
358
+ print(app.dict())
359
+ ```
360
+
361
+ The other non-excluded fields accessible outside of the wrapped app are listed
362
+ in the `AppDefinition` class in `schema.py`:
363
+
364
+ ```python
365
+ class AppDefinition(WithClassInfo, SerialModel, ABC):
366
+ ...
367
+
368
+ app_id: AppID
369
+
370
+ feedback_definitions: Sequence[FeedbackDefinition] = []
371
+
372
+ feedback_mode: FeedbackMode = FeedbackMode.WITH_APP_THREAD
373
+
374
+ root_class: Class
375
+
376
+ root_callable: ClassVar[FunctionOrMethod]
377
+
378
+ app: JSON
379
+ ```
380
+
381
+ Note that `app` is in both classes. This distinction between `App` and
382
+ `AppDefinition` here is that one corresponds to potentially non-serializable
383
+ python objects (`App`) and their serializable versions (`AppDefinition`).
384
+ Feedbacks should expect to be run with `AppDefinition`. Fields of `App` that are
385
+ not part of `AppDefinition` may not be available.
386
+
387
+ You can inspect the data available for feedback definitions in the dashboard by
388
+ clicking on the "See full app json" button on the bottom of the page after
389
+ selecting a record from a table.
390
+
391
+ The other piece of context to Feedback evaluation are records. These contain the
392
+ inputs/outputs and other information collected during the execution of an app:
393
+
394
+ ```python
395
+ class Record(SerialModel):
396
+ record_id: RecordID
397
+ app_id: AppID
398
+
399
+ cost: Optional[Cost] = None
400
+ perf: Optional[Perf] = None
401
+
402
+ ts: datetime = pydantic.Field(default_factory=lambda: datetime.now())
403
+
404
+ tags: str = ""
405
+
406
+ main_input: Optional[JSON] = None
407
+ main_output: Optional[JSON] = None # if no error
408
+ main_error: Optional[JSON] = None # if error
409
+
410
+ # The collection of calls recorded. Note that these can be converted into a
411
+ # json structure with the same paths as the app that generated this record
412
+ # via `layout_calls_as_app`.
413
+ calls: Sequence[RecordAppCall] = []
414
+ ```
415
+
416
+ A listing of a record can be seen in the dashboard by clicking the "see full
417
+ record json" button on the bottom of the page after selecting a record from the
418
+ table.
419
+
420
+ ### Calls made by App Components
421
+
422
+ When evaluating a feedback function, Records are augmented with
423
+ app/component calls in app layout in the attribute `app`. By this we mean that
424
+ in addition to the fields listed in the class definition above, the `app` field
425
+ will contain the same information as `calls` but organized in a manner mirroring
426
+ the organization of the app structure. For example, if the instrumented app
427
+ contains a component `combine_docs_chain` then `app.combine_docs_chain` will
428
+ contain calls to methods of this component. In the example at the top of this
429
+ docstring, `_call` was an example of such a method. Thus
430
+ `app.combine_docs_chain._call` further contains a `RecordAppCall` (see
431
+ schema.py) structure with information about the inputs/outputs/metadata
432
+ regarding the `_call` call to that component. Selecting this information is the
433
+ reason behind the `Select.RecordCalls` alias (see next section).
434
+
435
+ You can inspect the components making up your app via the `App` method
436
+ `print_instrumented`.
437
+