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