Skip to content

Commit 903a387

Browse files
varjoshiPouyanpi
andauthored
Patronus Evaluate API Integration (#834)
* Patronus Evaluate API Integration * Address comments - tests will be added separately * Add missing tests * Remove print statements --------- Signed-off-by: Pouyan <13303554+Pouyanpi@users.noreply.github.com> Co-authored-by: Pouyan <13303554+Pouyanpi@users.noreply.github.com>
1 parent b25eaec commit 903a387

File tree

9 files changed

+1290
-2
lines changed

9 files changed

+1290
-2
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Patronus Evaluate API Integration
2+
3+
NeMo Guardrails supports using [Patronus AI](www.patronus.ai)'s Evaluate API as an output rail. The Evaluate API gives you access to Patronus' powerful suite of fully-managed in-house evaluation models, including [Lynx](patronus-lynx.md), Judge (a hosted LLM-as-a-Judge model), Toxicity, PII, and PHI models, and a suite of specialized RAG evaluators with
4+
industry-leading performance on metrics like Answer Relevance, Context Relevance, Context Sufficiency, and Hallucination.
5+
6+
Patronus also has Managed configurations of the Judge evaluator, which you can use to detect AI failures like prompt injection and brand misalignment in order to prevent problematic bot responses from being returned to users.
7+
8+
## Setup
9+
10+
1. Sign up for an account on [app.patronus.ai](https://app.patronus.ai).
11+
2. You can follow the Quick Start guide [here](https://docs.patronus.ai/docs/quickstart-guide) to get onboarded.
12+
3. Create an API Key and save it somewhere safe.
13+
14+
## Usage
15+
16+
Here's how to use the Patronus Evaluate API as an output rail:
17+
18+
1. Get a Patronus API key and set it to the PATRONUS_API_KEY variable in your environment.
19+
20+
2. Add the guardrail `patronus api check output` to your output rails in `config.yml`:
21+
22+
```yaml
23+
rails:
24+
output:
25+
flows:
26+
- patronus api check output
27+
```
28+
29+
3. Add a rails config for Patronus in `config.yml`:
30+
31+
```yaml
32+
rails:
33+
config:
34+
patronus:
35+
output:
36+
evaluate_config:
37+
success_strategy: "all_pass"
38+
params:
39+
{
40+
evaluators:
41+
[
42+
{ "evaluator": "lynx" },
43+
{
44+
"evaluator": "answer-relevance",
45+
"explain_strategy": "on-fail",
46+
},
47+
],
48+
tags: { "retrieval_configuration": "ast-123" },
49+
}
50+
```
51+
52+
The `evaluate_config` has two top-level arguments: `success_strategy` and `params`.
53+
54+
In `params` you can pass the relevant arguments to the Patronus Evaluate API. The schema is the same as the API documentation [here](https://docs.patronus.ai/reference/evaluate_v1_evaluate_post), so as new API parameters are added and new values are supported, you can readily add them to your NeMo Guardrails configuration.
55+
56+
Note that you can pass in multiple evaluators to the Patronus Evaluate API. By setting `success_strategy` to "all_pass",
57+
every single evaluator called in the Evaluate API must pass for the rail to pass successfully. If you set it to "any_pass", then only one evaluator needs to pass.
58+
59+
## Additional Information
60+
61+
For now, the Evaluate API Integration only looks at whether the evaluators return Pass or Fail in the API response. However, most evaluators return a score between 0 and 1, where by default a score below 0.5 indicates a Fail and score above 0.5 indicates a Pass. But you can use the score directly to adjust how sensitive your pass/fail threshold should be. The API response can also include explanations of why the rail passed or failed that could be surfaced to a user (set `explain_strategy` in the evaluator object). Some evaluators even include spans of problematic keywords or sentences where issues like hallucinations are present, so you can scrub them out before returning the bot response.
62+
63+
Here's the `patronus api check output` flow, showing how the action is executed:
64+
65+
```colang
66+
define bot inform answer unknown
67+
"I don't know the answer to that."
68+
69+
define flow patronus api check output
70+
$patronus_response = execute PatronusApiCheckOutputAction
71+
$evaluation_passed = $patronus_response["pass"]
72+
73+
if not $evaluation_passed
74+
bot inform answer unknown
75+
```

docs/user_guides/llm-support.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ If you want to use an LLM and you cannot see a prompt in the [prompts folder](ht
3737
| Got It AI RAG TruthChecker _(LLM independent)_ | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
3838
| Patronus Lynx RAG Hallucination detection _(LLM independent)_ | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
3939
| GCP Text Moderation _(LLM independent)_ | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
40+
| Patronus Evaluate API _(LLM independent)_ | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
41+
4042

4143
Table legend:
4244
- :heavy_check_mark: - Supported (_The feature is fully supported by the LLM based on our experiments and tests_)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
models:
2+
- type: main
3+
engine: openai
4+
model: gpt-3.5-turbo-instruct
5+
6+
rails:
7+
output:
8+
flows:
9+
- patronus api check output
10+
config:
11+
patronus:
12+
output:
13+
evaluate_config:
14+
success_strategy: "all_pass"
15+
params:
16+
{
17+
evaluators:
18+
[
19+
{ "evaluator": "lynx" },
20+
{
21+
"evaluator": "answer-relevance",
22+
"explain_strategy": "on-fail",
23+
},
24+
],
25+
tags: { "hello": "world" },
26+
}

nemoguardrails/library/patronusai/actions.py

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
# limitations under the License.
1515

1616
import logging
17+
import os
1718
import re
18-
from typing import List, Optional, Tuple, Union
19+
from typing import List, Literal, Optional, Tuple, Union
1920

21+
import aiohttp
2022
from langchain_core.language_models.llms import BaseLLM
2123

2224
from nemoguardrails.actions import action
@@ -106,5 +108,140 @@ async def patronus_lynx_check_output_hallucination(
106108
)
107109

108110
hallucination, reasoning = parse_patronus_lynx_response(result)
109-
print(f"Hallucination: {hallucination}, Reasoning: {reasoning}")
110111
return {"hallucination": hallucination, "reasoning": reasoning}
112+
113+
114+
def check_guardrail_pass(
115+
response: Optional[dict], success_strategy: Literal["all_pass", "any_pass"]
116+
) -> bool:
117+
"""
118+
Check if evaluations in the Patronus API response pass based on the success strategy.
119+
"all_pass" requires all evaluators to pass for success.
120+
"any_pass" requires only one evaluator to pass for success.
121+
"""
122+
if not response or "results" not in response:
123+
return False
124+
125+
evaluations = response["results"]
126+
127+
if success_strategy == "all_pass":
128+
return all(
129+
"evaluation_result" in result
130+
and isinstance(result["evaluation_result"], dict)
131+
and result["evaluation_result"].get("pass", False)
132+
for result in evaluations
133+
)
134+
return any(
135+
"evaluation_result" in result
136+
and isinstance(result["evaluation_result"], dict)
137+
and result["evaluation_result"].get("pass", False)
138+
for result in evaluations
139+
)
140+
141+
142+
async def patronus_evaluate_request(
143+
api_params: dict,
144+
user_input: Optional[str] = None,
145+
bot_response: Optional[str] = None,
146+
provided_context: Optional[Union[str, List[str]]] = None,
147+
) -> Optional[dict]:
148+
"""
149+
Make a call to the Patronus Evaluate API.
150+
151+
Returns a dictionary of the API response JSON if successful, or None if a server error occurs.
152+
* Server errors will cause the guardrail to block the bot response
153+
154+
Raises a ValueError for client errors (400-499), as these indicate invalid requests.
155+
"""
156+
api_key = os.environ.get("PATRONUS_API_KEY")
157+
158+
if api_key is None:
159+
raise ValueError("PATRONUS_API_KEY environment variable not set.")
160+
161+
if "evaluators" not in api_params:
162+
raise ValueError(
163+
"The Patronus Evaluate API parameters must contain an 'evaluators' field"
164+
)
165+
evaluators = api_params["evaluators"]
166+
if not isinstance(evaluators, list):
167+
raise ValueError(
168+
"The Patronus Evaluate API parameter 'evaluators' must be a list"
169+
)
170+
171+
for evaluator in evaluators:
172+
if not isinstance(evaluator, dict):
173+
raise ValueError(
174+
"Each object in the 'evaluators' list must be a dictionary"
175+
)
176+
if "evaluator" not in evaluator:
177+
raise ValueError(
178+
"Each dictionary in the 'evaluators' list must contain the 'evaluator' field"
179+
)
180+
181+
data = {
182+
**api_params,
183+
"evaluated_model_input": user_input,
184+
"evaluated_model_output": bot_response,
185+
"evaluated_model_retrieved_context": provided_context,
186+
}
187+
188+
url = "https://api.patronus.ai/v1/evaluate"
189+
headers = {
190+
"X-API-KEY": api_key,
191+
"Content-Type": "application/json",
192+
}
193+
194+
async with aiohttp.ClientSession() as session:
195+
async with session.post(
196+
url=url,
197+
headers=headers,
198+
json=data,
199+
) as response:
200+
if 400 <= response.status < 500:
201+
raise ValueError(
202+
f"The Patronus Evaluate API call failed with status code {response.status}. "
203+
f"Details: {await response.text()}"
204+
)
205+
206+
if response.status != 200:
207+
log.error(
208+
"The Patronus Evaluate API call failed with status code %s. Details: %s",
209+
response.status,
210+
await response.text(),
211+
)
212+
return None
213+
214+
response_json = await response.json()
215+
return response_json
216+
217+
218+
@action(name="patronus_api_check_output")
219+
async def patronus_api_check_output(
220+
llm_task_manager: LLMTaskManager,
221+
context: Optional[dict] = None,
222+
) -> dict:
223+
"""
224+
Check the user message, bot response, and/or provided context
225+
for issues based on the Patronus Evaluate API
226+
"""
227+
user_input = context.get("user_message")
228+
bot_response = context.get("bot_message")
229+
provided_context = context.get("relevant_chunks")
230+
231+
patronus_config = llm_task_manager.config.rails.config.patronus.output
232+
evaluate_config = getattr(patronus_config, "evaluate_config", {})
233+
success_strategy: Literal["all_pass", "any_pass"] = getattr(
234+
evaluate_config, "success_strategy", "all_pass"
235+
)
236+
api_params = getattr(evaluate_config, "params", {})
237+
response = await patronus_evaluate_request(
238+
api_params=api_params,
239+
user_input=user_input,
240+
bot_response=bot_response,
241+
provided_context=provided_context,
242+
)
243+
return {
244+
"pass": check_guardrail_pass(
245+
response=response, success_strategy=success_strategy
246+
)
247+
}

nemoguardrails/library/patronusai/flows.co

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ flow patronus lynx check output hallucination
1313
else
1414
bot inform answer unknown
1515
abort
16+
17+
flow patronus api check output
18+
$patronus_response = await PatronusApiCheckOutputAction
19+
global $evaluation_passed
20+
$evaluation_passed = $patronus_response["pass"]
21+
22+
if not $evaluation_passed
23+
bot inform answer unknown

nemoguardrails/library/patronusai/flows.v1.co

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ define flow patronus lynx check output hallucination
1313
else
1414
bot inform answer unknown
1515
stop
16+
17+
define flow patronus api check output
18+
$patronus_response = execute PatronusApiCheckOutputAction
19+
$evaluation_passed = $patronus_response["pass"]
20+
21+
if not $evaluation_passed
22+
bot inform answer unknown

nemoguardrails/rails/llm/config.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import logging
1919
import os
2020
import warnings
21+
from enum import Enum
2122
from typing import Any, Dict, List, Optional, Set, Tuple, Union
2223

2324
import yaml
@@ -392,6 +393,54 @@ class AutoAlignRailConfig(BaseModel):
392393
)
393394

394395

396+
class PatronusEvaluationSuccessStrategy(str, Enum):
397+
"""
398+
Strategy for determining whether a Patronus Evaluation API
399+
request should pass, especially when multiple evaluators
400+
are called in a single request.
401+
ALL_PASS requires all evaluators to pass for success.
402+
ANY_PASS requires only one evaluator to pass for success.
403+
"""
404+
405+
ALL_PASS = "all_pass"
406+
ANY_PASS = "any_pass"
407+
408+
409+
class PatronusEvaluateApiParams(BaseModel):
410+
"""Config to parameterize the Patronus Evaluate API call"""
411+
412+
success_strategy: Optional[PatronusEvaluationSuccessStrategy] = Field(
413+
default=PatronusEvaluationSuccessStrategy.ALL_PASS,
414+
description="Strategy to determine whether the Patronus Evaluate API Guardrail passes or not.",
415+
)
416+
params: Dict[str, Any] = Field(
417+
default_factory=dict,
418+
description="Parameters to the Patronus Evaluate API",
419+
)
420+
421+
422+
class PatronusEvaluateConfig(BaseModel):
423+
"""Config for the Patronus Evaluate API call"""
424+
425+
evaluate_config: PatronusEvaluateApiParams = Field(
426+
default_factory=PatronusEvaluateApiParams,
427+
description="Configuration passed to the Patronus Evaluate API",
428+
)
429+
430+
431+
class PatronusRailConfig(BaseModel):
432+
"""Configuration data for the Patronus Evaluate API"""
433+
434+
input: Optional[PatronusEvaluateConfig] = Field(
435+
default_factory=PatronusEvaluateConfig,
436+
description="Patronus Evaluate API configuration for an Input Guardrail",
437+
)
438+
output: Optional[PatronusEvaluateConfig] = Field(
439+
default_factory=PatronusEvaluateConfig,
440+
description="Patronus Evaluate API configuration for an Output Guardrail",
441+
)
442+
443+
395444
class RailsConfigData(BaseModel):
396445
"""Configuration data for specific rails that are supported out-of-the-box."""
397446

@@ -405,6 +454,11 @@ class RailsConfigData(BaseModel):
405454
description="Configuration data for the AutoAlign guardrails API.",
406455
)
407456

457+
patronus: Optional[PatronusRailConfig] = Field(
458+
default_factory=PatronusRailConfig,
459+
description="Configuration data for the Patronus Evaluate API.",
460+
)
461+
408462
sensitive_data_detection: Optional[SensitiveDataDetection] = Field(
409463
default_factory=SensitiveDataDetection,
410464
description="Configuration for detecting sensitive data.",

0 commit comments

Comments
 (0)