trulens-apps-nemo 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,26 @@
1
+ Metadata-Version: 2.1
2
+ Name: trulens-apps-nemo
3
+ Version: 1.0.1
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,<3.12
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
+ Requires-Dist: nemoguardrails (>=0.9,<0.10)
18
+ Requires-Dist: pydantic (>=2,<3)
19
+ Requires-Dist: trulens-apps-langchain (>=1.0.0,<2.0.0) ; python_version < "3.13"
20
+ Requires-Dist: trulens-core (>=1.0.0,<2.0.0)
21
+ Project-URL: Documentation, https://trulens.org/trulens/getting_started/
22
+ Project-URL: Repository, https://github.com/truera/trulens
23
+ Description-Content-Type: text/markdown
24
+
25
+ # trulens-apps-nemo
26
+
@@ -0,0 +1 @@
1
+ # trulens-apps-nemo
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ build-backend = "poetry.core.masonry.api"
3
+ requires = [
4
+ "poetry-core",
5
+ ]
6
+
7
+ [tool.poetry]
8
+ name = "trulens-apps-nemo"
9
+ version = "1.0.1"
10
+ description = "Library to systematically track and evaluate LLM based applications."
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/trulens/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,<3.12"
31
+ trulens-core = { version = "^1.0.0" }
32
+ trulens-apps-langchain = { version = "^1.0.0", python = "<3.13" }
33
+ pydantic = "^2"
34
+ nemoguardrails = "^0.9"
35
+
36
+ [tool.poetry.group.dev.dependencies]
37
+ trulens-core = { path = "../../core" }
38
+ trulens-apps-langchain = { path = "../langchain", python = "<3.13" }
@@ -0,0 +1,21 @@
1
+ """
2
+ !!! note "Additional Dependency Required"
3
+
4
+ To use this module, you must have the `trulens-apps-nemo` package installed.
5
+
6
+ ```bash
7
+ pip install trulens-apps-nemo
8
+ ```
9
+ """
10
+
11
+ from importlib.metadata import version
12
+
13
+ from trulens.apps.nemo.tru_rails import RailsActionSelect
14
+ from trulens.apps.nemo.tru_rails import RailsInstrument
15
+ from trulens.apps.nemo.tru_rails import TruRails
16
+ from trulens.core.utils.imports import safe_importlib_package_name
17
+
18
+ __version__ = version(safe_importlib_package_name(__package__ or __name__))
19
+
20
+
21
+ __all__ = ["TruRails", "RailsInstrument", "RailsActionSelect"]
File without changes
@@ -0,0 +1,448 @@
1
+ """
2
+ NeMo Guardrails instrumentation and monitoring.
3
+ """
4
+
5
+ import inspect
6
+ from inspect import BoundArguments
7
+ from inspect import Signature
8
+ import logging
9
+ from pprint import pformat
10
+ from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple, Union
11
+
12
+ from langchain_core.language_models.base import BaseLanguageModel
13
+ from nemoguardrails import LLMRails
14
+ from nemoguardrails import RailsConfig
15
+ from nemoguardrails.actions.action_dispatcher import ActionDispatcher
16
+ from nemoguardrails.actions.actions import ActionResult
17
+ from nemoguardrails.actions.actions import action
18
+ from nemoguardrails.actions.llm.generation import LLMGenerationActions
19
+ from nemoguardrails.kb.kb import KnowledgeBase
20
+ from pydantic import Field
21
+ from trulens.apps.langchain import LangChainInstrument
22
+ from trulens.core import app as mod_app
23
+ from trulens.core import feedback
24
+ from trulens.core.instruments import ClassFilter
25
+ from trulens.core.instruments import Instrument
26
+ from trulens.core.schema import Select
27
+ from trulens.core.utils.containers import dict_set_with_multikey
28
+ from trulens.core.utils.json import jsonify
29
+ from trulens.core.utils.pyschema import Class
30
+ from trulens.core.utils.pyschema import FunctionOrMethod
31
+ from trulens.core.utils.python import safe_hasattr
32
+ from trulens.core.utils.serial import JSON
33
+ from trulens.core.utils.serial import Lens
34
+ from trulens.core.utils.text import retab
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class RailsActionSelect(Select):
40
+ """Selector shorthands for _NeMo Guardrails_ apps when used for evaluating
41
+ feedback in actions.
42
+
43
+ These should not be used for feedback functions given to `TruRails` but
44
+ instead for selectors in the `FeedbackActions` action invoked from with a
45
+ rails app.
46
+ """
47
+
48
+ Action = Lens().action
49
+ """Selector for action call parameters."""
50
+
51
+ Events = Action.events
52
+ """Selector for events in action call parameters."""
53
+
54
+ Context = Action.context
55
+ """Selector for context in action call parameters.
56
+
57
+ Warning:
58
+ This is not the same "context" as in RAG triad. This is a parameter to rails
59
+ actions that stores context of the rails app execution.
60
+ """
61
+
62
+ LLM = Action.llm
63
+ """Selector for the language model in action call parameters."""
64
+
65
+ Config = Action.config
66
+ """Selector for the configuration in action call parameters."""
67
+
68
+ RetrievalContexts = Context.relevant_chunks_sep
69
+ """Selector for the retrieved contexts chunks returned from a KB search.
70
+
71
+ Equivalent to `$relevant_chunks_sep` in colang."""
72
+
73
+ UserMessage = Context.user_message
74
+ """Selector for the user message.
75
+
76
+ Equivalent to `$user_message` in colang."""
77
+
78
+ BotMessage = Context.bot_message
79
+ """Selector for the bot message.
80
+
81
+ Equivalent to `$bot_message` in colang."""
82
+
83
+ LastUserMessage = Context.last_user_message
84
+ """Selector for the last user message.
85
+
86
+ Equivalent to `$last_user_message` in colang."""
87
+
88
+ LastBotMessage = Context.last_bot_message
89
+ """Selector for the last bot message.
90
+
91
+ Equivalent to `$last_bot_message` in colang."""
92
+
93
+
94
+ # NOTE(piotrm): Cannot have this inside FeedbackActions presently due to perhaps
95
+ # some closure-related issues with the @action decorator below.
96
+ registered_feedback_functions = {}
97
+
98
+
99
+ class FeedbackActions:
100
+ """Feedback action action for _NeMo Guardrails_ apps.
101
+
102
+ See docstring of method `feedback`.
103
+ """
104
+
105
+ @staticmethod
106
+ def register_feedback_functions(
107
+ *args: Tuple[feedback.Feedback, ...],
108
+ **kwargs: Dict[str, feedback.Feedback],
109
+ ):
110
+ """Register one or more feedback functions to use in rails `feedback`
111
+ action.
112
+
113
+ All keyword arguments indicate the key as the keyword. All
114
+ positional arguments use the feedback name as the key.
115
+ """
116
+
117
+ for name, feedback_instance in kwargs.items():
118
+ if not isinstance(feedback_instance, feedback.Feedback):
119
+ raise ValueError(
120
+ f"Invalid feedback function: {feedback_instance}; "
121
+ f"expected a Feedback class instance."
122
+ )
123
+ print(f"registered feedback function under name {name}")
124
+ registered_feedback_functions[name] = feedback_instance
125
+
126
+ for feedback_instance in args:
127
+ if not isinstance(feedback_instance, feedback.Feedback):
128
+ raise ValueError(
129
+ f"Invalid feedback function: {feedback_instance}; "
130
+ f"expected a Feedback class instance."
131
+ )
132
+ print(
133
+ f"registered feedback function under name {feedback_instance.name}"
134
+ )
135
+ registered_feedback_functions[feedback_instance.name] = feedback
136
+
137
+ @staticmethod
138
+ def action_of_feedback(
139
+ feedback_instance: feedback.Feedback, verbose: bool = False
140
+ ) -> Callable:
141
+ """Create a custom rails action for the given feedback function.
142
+
143
+ Args:
144
+ feedback_instance: A feedback function to register as an action.
145
+
146
+ verbose: Print out info on invocation upon invocation.
147
+
148
+ Returns:
149
+ A custom action that will run the feedback function. The name is
150
+ the same as the feedback function's name.
151
+ """
152
+
153
+ if not isinstance(feedback_instance, feedback.Feedback):
154
+ raise ValueError(
155
+ f"Invalid feedback function: {feedback_instance}; "
156
+ f"expected a Feedback class instance."
157
+ )
158
+
159
+ @action(name=feedback_instance.name)
160
+ async def run_feedback(*args, **kwargs):
161
+ if verbose:
162
+ print(
163
+ f"Running feedback function {feedback_instance.name} with:"
164
+ )
165
+ print(f" args = {args}")
166
+ print(f" kwargs = {kwargs}")
167
+
168
+ res = feedback_instance.run(*args, **kwargs).result
169
+
170
+ if verbose:
171
+ print(f" result = {res}")
172
+
173
+ return res
174
+
175
+ return run_feedback
176
+
177
+ @action(name="feedback")
178
+ @staticmethod
179
+ async def feedback_action(
180
+ # Default action arguments:
181
+ events: Optional[List[Dict]] = None,
182
+ context: Optional[Dict] = None,
183
+ llm: Optional[BaseLanguageModel] = None,
184
+ config: Optional[RailsConfig] = None,
185
+ # Rest of arguments are specific to this action.
186
+ function: Optional[str] = None,
187
+ selectors: Optional[Dict[str, Union[str, Lens]]] = None,
188
+ verbose: bool = False,
189
+ ) -> ActionResult:
190
+ """Run the specified feedback function from trulens.
191
+
192
+ To use this action, it needs to be registered with your rails app and
193
+ feedback functions themselves need to be registered with this function.
194
+ The name under which this action is registered for rails is `feedback`.
195
+
196
+ Usage:
197
+ ```python
198
+ rails: LLMRails = ... # your app
199
+ language_match: Feedback = Feedback(...) # your feedback function
200
+
201
+ # First we register some feedback functions with the custom action:
202
+ FeedbackAction.register_feedback_functions(language_match)
203
+
204
+ # Can also use kwargs expansion from dict like produced by rag_triad:
205
+ # FeedbackAction.register_feedback_functions(**rag_triad(...))
206
+
207
+ # Then the feedback method needs to be registered with the rails app:
208
+ rails.register_action(FeedbackAction.feedback)
209
+ ```
210
+
211
+ Args:
212
+ events: See [Action
213
+ parameters](https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/user_guides/python-api.md#special-parameters).
214
+
215
+ context: See [Action
216
+ parameters](https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/user_guides/python-api.md#special-parameters).
217
+
218
+ llm: See [Action
219
+ parameters](https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/user_guides/python-api.md#special-parameters).
220
+
221
+ config: See [Action
222
+ parameters](https://github.com/NVIDIA/NeMo-Guardrails/blob/develop/docs/user_guides/python-api.md#special-parameters).
223
+
224
+ function: Name of the feedback function to run.
225
+
226
+ selectors: Selectors for the function. Can be provided either as
227
+ strings to be parsed into lenses or lenses themselves.
228
+
229
+ verbose: Print the values of the selectors before running feedback
230
+ and print the result after running feedback.
231
+
232
+ Returns:
233
+ ActionResult: An action result containing the result of the feedback.
234
+
235
+ Example:
236
+ ```colang
237
+ define subflow check language match
238
+ $result = execute feedback(\\
239
+ function="language_match",\\
240
+ selectors={\\
241
+ "text1":"action.context.last_user_message",\\
242
+ "text2":"action.context.bot_message"\\
243
+ }\\
244
+ )
245
+ if $result < 0.8
246
+ bot inform language mismatch
247
+ stop
248
+ ```
249
+ """
250
+
251
+ feedback_function = registered_feedback_functions.get(function)
252
+
253
+ if feedback_function is None:
254
+ raise ValueError(
255
+ f"Invalid feedback function: {function}; "
256
+ f"there is/are {len(registered_feedback_functions)} registered function(s):\n\t"
257
+ + "\n\t".join(registered_feedback_functions.keys())
258
+ + "\n"
259
+ )
260
+
261
+ fname = feedback_function.name
262
+
263
+ if selectors is None:
264
+ raise ValueError(
265
+ f"Need selectors for feedback function: {fname} "
266
+ f"with signature {inspect.signature(feedback_function.imp)}"
267
+ )
268
+
269
+ selectors = {
270
+ argname: (
271
+ Lens.of_string(arglens) if isinstance(arglens, str) else arglens
272
+ )
273
+ for argname, arglens in selectors.items()
274
+ }
275
+
276
+ feedback_function = feedback_function.on(**selectors)
277
+
278
+ source_data = dict(
279
+ action=dict(events=events, context=context, llm=llm, config=config)
280
+ )
281
+
282
+ if verbose:
283
+ print(fname)
284
+ for argname, lens in feedback_function.selectors.items():
285
+ print(f" {argname} = ", end=None)
286
+ # use pretty print for the potentially big thing here:
287
+ print(
288
+ retab(
289
+ tab=" ", s=pformat(lens.get_sole_item(source_data))
290
+ )
291
+ )
292
+
293
+ context_updates = {}
294
+
295
+ try:
296
+ result = feedback_function.run(source_data=source_data)
297
+ context_updates["result"] = result.result
298
+
299
+ if verbose:
300
+ print(f" {fname} result = {result.result}")
301
+
302
+ except Exception:
303
+ context_updates["result"] = None
304
+
305
+ return ActionResult(
306
+ return_value=context_updates["result"],
307
+ context_updates=context_updates,
308
+ )
309
+
310
+ return ActionResult(
311
+ return_value=context_updates["result"],
312
+ context_updates=context_updates,
313
+ )
314
+
315
+
316
+ class RailsInstrument(Instrument):
317
+ """Instrumentation specification for _NeMo Guardrails_ apps."""
318
+
319
+ class Default:
320
+ """Default instrumentation specification."""
321
+
322
+ MODULES = {"nemoguardrails"}.union(LangChainInstrument.Default.MODULES)
323
+ """Modules to instrument by name prefix.
324
+
325
+ Note that _NeMo Guardrails_ uses _LangChain_ internally for some things.
326
+ """
327
+
328
+ CLASSES = lambda: {
329
+ LLMRails,
330
+ KnowledgeBase,
331
+ LLMGenerationActions,
332
+ ActionDispatcher,
333
+ FeedbackActions,
334
+ }.union(LangChainInstrument.Default.CLASSES())
335
+ """Instrument only these classes."""
336
+
337
+ METHODS: Dict[str, ClassFilter] = dict_set_with_multikey(
338
+ dict(LangChainInstrument.Default.METHODS), # copy
339
+ {
340
+ ("execute_action"): ActionDispatcher,
341
+ (
342
+ "generate",
343
+ "generate_async",
344
+ "stream_async",
345
+ "generate_events",
346
+ "generate_events_async",
347
+ "_get_events_for_messages",
348
+ ): LLMRails,
349
+ "search_relevant_chunks": KnowledgeBase,
350
+ (
351
+ "generate_user_intent",
352
+ "generate_next_step",
353
+ "generate_bot_message",
354
+ "generate_value",
355
+ "generate_intent_steps_message",
356
+ ): LLMGenerationActions,
357
+ # TODO: Include feedback method in FeedbackActions, currently
358
+ # bugged and will not be logged.
359
+ "feedback": FeedbackActions,
360
+ },
361
+ )
362
+ """Instrument only methods with these names and of these classes."""
363
+
364
+ def __init__(self, *args, **kwargs):
365
+ super().__init__(
366
+ include_modules=RailsInstrument.Default.MODULES,
367
+ include_classes=RailsInstrument.Default.CLASSES(),
368
+ include_methods=RailsInstrument.Default.METHODS,
369
+ *args,
370
+ **kwargs,
371
+ )
372
+
373
+
374
+ class TruRails(mod_app.App):
375
+ """Recorder for apps defined using _NeMo Guardrails_.
376
+
377
+ Args:
378
+ app: A _NeMo Guardrails_ application.
379
+ """
380
+
381
+ model_config: ClassVar[dict] = {"arbitrary_types_allowed": True}
382
+
383
+ app: LLMRails
384
+
385
+ root_callable: ClassVar[FunctionOrMethod] = Field(
386
+ default_factory=lambda: FunctionOrMethod.of_callable(LLMRails.generate)
387
+ )
388
+
389
+ def __init__(self, app: LLMRails, **kwargs):
390
+ # TruLlama specific:
391
+ kwargs["app"] = app
392
+ kwargs["root_class"] = Class.of_object(app) # TODO: make class property
393
+ kwargs["instrument"] = RailsInstrument(app=self)
394
+
395
+ super().__init__(**kwargs)
396
+
397
+ def main_output(
398
+ self, func: Callable, sig: Signature, bindings: BoundArguments, ret: Any
399
+ ) -> JSON:
400
+ """
401
+ Determine the main out string for the given function `func` with
402
+ signature `sig` after it is called with the given `bindings` and has
403
+ returned `ret`.
404
+ """
405
+
406
+ if isinstance(ret, dict):
407
+ if "content" in ret:
408
+ return ret["content"]
409
+
410
+ return jsonify(ret)
411
+
412
+ def main_input(
413
+ self, func: Callable, sig: Signature, bindings: BoundArguments
414
+ ) -> JSON:
415
+ """
416
+ Determine the main input string for the given function `func` with
417
+ signature `sig` after it is called with the given `bindings` and has
418
+ returned `ret`.
419
+ """
420
+
421
+ if "messages" in bindings.arguments:
422
+ messages = bindings.arguments["messages"]
423
+ if len(messages) == 1:
424
+ message = messages[0]
425
+ if "content" in message:
426
+ return message["content"]
427
+
428
+ return jsonify(bindings.arguments)
429
+
430
+ @classmethod
431
+ def select_context(cls, app: Optional[LLMRails] = None) -> Lens:
432
+ """
433
+ Get the path to the context in the query output.
434
+ """
435
+ return Select.RecordCalls.kb.search_relevant_chunks.rets[:].body
436
+
437
+ def __getattr__(self, name):
438
+ if name == "__name__":
439
+ return self.__class__.__name__ # Return the class name of TruRails
440
+ elif safe_hasattr(self.app, name):
441
+ return getattr(
442
+ self.app, name
443
+ ) # Delegate to the wrapped app if it has the attribute
444
+ else:
445
+ raise AttributeError(f"TruRails has no attribute named {name}")
446
+
447
+
448
+ TruRails.model_rebuild()