Skip to content

Commit 3a167a0

Browse files
authored
Merge branch 'dependabot-updates-march-28' into dependabot/pip/pip-e49d2f513e
2 parents 8c02ed9 + 2431a70 commit 3a167a0

File tree

13 files changed

+346
-68
lines changed

13 files changed

+346
-68
lines changed

.github/workflows/autoclose_stale_issues_and_prs.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ jobs:
1919
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
2020
close-issue-message: 'This issue was closed because it has been stalled for 14 days with no activity.'
2121
close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.'
22-
days-before-issue-stale: 30
23-
days-before-pr-stale: 30
24-
days-before-issue-close: 14
25-
days-before-pr-close: 14
22+
days-before-issue-stale: 60
23+
days-before-pr-stale: 60
24+
days-before-issue-close: 30
25+
days-before-pr-close: 30
2626
repo-token: ${{ secrets.GITHUB_TOKEN }}
27-
operations-per-run: 300
27+
operations-per-run: 300

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
</div>
2121

22+
## News and Updates
23+
- **[Feb 12, 2025]** We just launched Guardrails Index -- the first of its kind benchmark comparing the performance and latency of 24 guardrails across 6 most common categories! Check out the index at index.guardrailsai.com
24+
2225
## What is Guardrails?
2326

2427
Guardrails is a Python framework that helps build reliable AI applications by performing two key functions:

docs/faq.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,25 @@ If your login issues persist, please check the contents of the ~/.guardrailsrc f
129129
If you're still encountering issues, please [open an issue](https://github.com/guardrails-ai/guardrails/issues/new) and we'll help you out!
130130

131131
We're also available on [Discord](https://discord.gg/U9RKkZSBgx) if you want to chat with us directly.
132+
133+
## I'm getting an error related to distutils when installing validators.
134+
This can happen on cuda enabled devices in python versions 3.11 and below when a validator indirectly depends on a package that imports distutils.
135+
136+
If you see an error similar to the one below:
137+
```sh
138+
Installing hub://guardrails/nsfw_text...
139+
[ =] Running post-install setup
140+
Device set to use cpu
141+
/home/ubuntu/support/.venv/lib/python3.11/site-packages/_distutils_hack/__init__.py:18: UserWarning: Distutils was imported before Setuptools, but importing Setuptools also replaces the `distutils` module in `sys.modules`. This may lead to undesirable behaviors or errors. To avoid these issues, avoid using distutils directly, ensure that setuptools is installed in the traditional way (e.g. not an editable install), and/or make sure that setuptools is always imported before distutils.
142+
warnings.warn(
143+
/home/ubuntu/support/.venv/lib/python3.11/site-packages/_distutils_hack/__init__.py:33: UserWarning: Setuptools is replacing distutils.
144+
warnings.warn("Setuptools is replacing distutils.")
145+
ERROR:guardrails-cli:Failed to import transformers.pipelines because of the following error (look up to see its traceback):
146+
/home/ubuntu/.pyenv/versions/3.11.11/lib/python3.11/distutils/core.py
147+
```
148+
149+
set the following as an environment variable to tell python to use the builtin version of distutils that exists in 3.11 and below:
150+
151+
```sh
152+
export SETUPTOOLS_USE_DISTUTILS="stdlib"
153+
```

guardrails/telemetry/common.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from typing import Any, Callable, Dict, Optional, Union
2+
from typing import Any, Callable, Dict, Optional, Union, List
33
from opentelemetry.baggage import get_baggage
44
from opentelemetry import context
55
from opentelemetry.context import Context
@@ -124,3 +124,105 @@ def add_user_attributes(span: Span):
124124
except Exception as e:
125125
logger.warning("Error loading baggage user information", e)
126126
pass
127+
128+
129+
def redact(value: str) -> str:
130+
"""Redacts all but the last four characters of the given string.
131+
132+
Args:
133+
value (str): The string to be redacted.
134+
135+
Returns:
136+
str: The redacted string with all but the last four characters
137+
replaced by asterisks.
138+
"""
139+
redaction_length = len(value) - 4
140+
stars = "*" * redaction_length
141+
return f"{stars}{value[-4:]}"
142+
143+
144+
def ismatchingkey(
145+
target_key: str,
146+
keys_to_match: tuple[str, ...] = ("key", "token", "password"),
147+
) -> bool:
148+
"""Check if the target key contains any of the specified keys to match.
149+
150+
Args:
151+
target_key (str): The key to be checked.
152+
keys_to_match (tuple[str, ...], optional): A tuple of keys to match
153+
against the target key. Defaults to ("key", "token").
154+
155+
Returns:
156+
bool: True if any of the keys to match are found in the target key,
157+
False otherwise.
158+
"""
159+
for k in keys_to_match:
160+
if k in target_key:
161+
return True
162+
return False
163+
164+
165+
def can_convert_to_dict(s: str) -> bool:
166+
"""Check if a string can be converted to a dictionary.
167+
168+
This function attempts to load the input string as JSON. If successful,
169+
it returns True, indicating that the string can be converted to a dictionary.
170+
Otherwise, it catches ValueError and TypeError exceptions and returns False.
171+
172+
Args:
173+
s (str): The input string to be checked.
174+
175+
Returns:
176+
bool: True if the string can be converted to a dictionary, False otherwise.
177+
"""
178+
try:
179+
json.loads(s)
180+
return True
181+
except (ValueError, TypeError):
182+
return False
183+
184+
185+
def recursive_key_operation(
186+
data: Optional[Union[Dict[str, Any], List[Any], str]],
187+
operation: Callable[[str], str],
188+
keys_to_match: List[str] = ["key", "token", "password"],
189+
) -> Optional[Union[Dict[str, Any], List[Any], str]]:
190+
"""Recursively traverses a dictionary, list, or JSON string and applies a
191+
specified operation to the values of keys that match any in the
192+
`keys_to_match` list. This function is useful for masking sensitive data
193+
(e.g., keys, tokens, passwords) in nested structures.
194+
195+
Args:
196+
data (Optional[Union[Dict[str, Any], List[Any], str]]): The input data
197+
to traverse. This can bea dictionary, list, or JSON string. If a
198+
JSON string is provided, it will be parsed into a dictionary before
199+
processing.
200+
201+
operation (Callable[[str], str]): A function that takes a string value
202+
and returns a modified string. This operation is applied to the values
203+
of keys that match any in `keys_to_match`.
204+
keys_to_match (List[str]): A list of keys to search for in the data. If
205+
a key matche any in this list, the corresponding value will be processed
206+
by the `operation`. Defaults to ["key", "token", "password"].
207+
208+
Returns:
209+
Optional[Union[Dict[str, Any], List[Any], str]]: The modified data structure
210+
with the operation applied to the values of matched keys. The return type
211+
matches the input type (dict, list, or str).
212+
"""
213+
if isinstance(data, str) and can_convert_to_dict(data):
214+
data_dict = json.loads(data)
215+
data = str(recursive_key_operation(data_dict, operation, keys_to_match))
216+
elif isinstance(data, dict):
217+
for key, value in data.items():
218+
if ismatchingkey(key, tuple(keys_to_match)) and isinstance(value, str):
219+
# Apply the operation to the value of the matched key
220+
data[key] = operation(value)
221+
else:
222+
# Recursively process nested dictionaries or lists
223+
data[key] = recursive_key_operation(value, operation, keys_to_match)
224+
elif isinstance(data, list):
225+
for i in range(len(data)):
226+
data[i] = recursive_key_operation(data[i], operation, keys_to_match)
227+
228+
return data

guardrails/telemetry/guard_tracing.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
Union,
1111
)
1212

13+
try:
14+
from openinference.semconv.trace import SpanAttributes # type: ignore
15+
except ImportError:
16+
SpanAttributes = None
1317
from opentelemetry import context, trace
1418
from opentelemetry.trace import StatusCode, Tracer, Span, Link, get_tracer
1519

@@ -153,6 +157,10 @@ def trace_stream_guard(
153157
guard_span = new_span
154158
add_guard_attributes(guard_span, history, res)
155159
add_user_attributes(guard_span)
160+
if SpanAttributes is not None:
161+
new_span.set_attribute(
162+
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
163+
)
156164
yield res
157165
except StopIteration:
158166
next_exists = False
@@ -179,7 +187,10 @@ def trace_guard_execution(
179187
guard_span.set_attribute("guardrails.version", GUARDRAILS_VERSION)
180188
guard_span.set_attribute("type", "guardrails/guard")
181189
guard_span.set_attribute("guard.name", guard_name)
182-
190+
if SpanAttributes is not None:
191+
guard_span.set_attribute(
192+
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
193+
)
183194
try:
184195
result = _execute_fn(*args, **kwargs)
185196
if isinstance(result, Iterator) and not isinstance(
@@ -218,6 +229,10 @@ async def trace_async_stream_guard(
218229

219230
add_guard_attributes(guard_span, history, res)
220231
add_user_attributes(guard_span)
232+
if SpanAttributes is not None:
233+
guard_span.set_attribute(
234+
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
235+
)
221236
yield res
222237
except StopIteration:
223238
next_exists = False
@@ -259,7 +274,10 @@ async def trace_async_guard_execution(
259274
guard_span.set_attribute("guardrails.version", GUARDRAILS_VERSION)
260275
guard_span.set_attribute("type", "guardrails/guard")
261276
guard_span.set_attribute("guard.name", guard_name)
262-
277+
if SpanAttributes is not None:
278+
guard_span.set_attribute(
279+
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
280+
)
263281
try:
264282
result = await _execute_fn(*args, **kwargs)
265283
if isinstance(result, AsyncIterator):

guardrails/telemetry/open_inference.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
import json
12
from typing import Any, Dict, List, Optional
23

3-
from guardrails.telemetry.common import get_span, to_dict, serialize
4+
from guardrails.telemetry.common import (
5+
get_span,
6+
to_dict,
7+
serialize,
8+
recursive_key_operation,
9+
redact,
10+
)
11+
12+
try:
13+
from openinference.semconv.trace import SpanAttributes # type: ignore
14+
except ImportError:
15+
SpanAttributes = None
416

517

618
def trace_operation(
@@ -75,7 +87,8 @@ def trace_llm_call(
7587

7688
if current_span is None:
7789
return
78-
90+
if SpanAttributes is not None:
91+
current_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL")
7992
ser_function_call = serialize(function_call)
8093
if ser_function_call:
8194
current_span.set_attribute("llm.function_call", ser_function_call)
@@ -92,9 +105,18 @@ def trace_llm_call(
92105
)
93106

94107
ser_invocation_parameters = serialize(invocation_parameters)
95-
if ser_invocation_parameters:
108+
redacted_ser_invocation_parameters = recursive_key_operation(
109+
ser_invocation_parameters, redact
110+
)
111+
reser_invocation_parameters = (
112+
json.dumps(redacted_ser_invocation_parameters)
113+
if isinstance(redacted_ser_invocation_parameters, dict)
114+
or isinstance(redacted_ser_invocation_parameters, list)
115+
else redacted_ser_invocation_parameters
116+
)
117+
if reser_invocation_parameters:
96118
current_span.set_attribute(
97-
"llm.invocation_parameters", ser_invocation_parameters
119+
"llm.invocation_parameters", reser_invocation_parameters
98120
)
99121

100122
ser_model_name = serialize(model_name)

guardrails/telemetry/runner_tracing.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
Optional,
99
)
1010

11+
try:
12+
from openinference.semconv.trace import SpanAttributes # type: ignore
13+
except ImportError:
14+
SpanAttributes = None
15+
1116
from opentelemetry import context, trace
1217
from opentelemetry.trace import StatusCode, Span
1318

@@ -17,7 +22,13 @@
1722
from guardrails.classes.output_type import OT
1823
from guardrails.classes.validation_outcome import ValidationOutcome
1924
from guardrails.stores.context import get_guard_name
20-
from guardrails.telemetry.common import get_tracer, add_user_attributes, serialize
25+
from guardrails.telemetry.common import (
26+
get_tracer,
27+
add_user_attributes,
28+
serialize,
29+
recursive_key_operation,
30+
redact,
31+
)
2132
from guardrails.utils.safe_get import safe_get
2233
from guardrails.version import GUARDRAILS_VERSION
2334

@@ -45,10 +56,14 @@ def add_step_attributes(
4556

4657
ser_args = [serialize(arg) for arg in args]
4758
ser_kwargs = {k: serialize(v) for k, v in kwargs.items()}
59+
4860
inputs = {
4961
"args": [sarg for sarg in ser_args if sarg is not None],
5062
"kwargs": {k: v for k, v in ser_kwargs.items() if v is not None},
5163
}
64+
for k in inputs:
65+
inputs[k] = recursive_key_operation(inputs[k], redact)
66+
5267
step_span.set_attribute("input.mime_type", "application/json")
5368
step_span.set_attribute("input.value", json.dumps(inputs))
5469

@@ -73,6 +88,10 @@ def trace_step_wrapper(*args, **kwargs) -> Iteration:
7388
name="step", # type: ignore
7489
context=current_otel_context, # type: ignore
7590
) as step_span:
91+
if SpanAttributes is not None:
92+
step_span.set_attribute(
93+
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
94+
)
7695
try:
7796
response = fn(*args, **kwargs)
7897
add_step_attributes(step_span, response, *args, **kwargs)
@@ -101,6 +120,8 @@ def trace_stream_step_generator(
101120
name="step", # type: ignore
102121
context=current_otel_context, # type: ignore
103122
) as step_span:
123+
if SpanAttributes is not None:
124+
step_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL")
104125
try:
105126
gen = fn(*args, **kwargs)
106127
next_exists = True
@@ -147,10 +168,15 @@ async def trace_async_step_wrapper(*args, **kwargs) -> Iteration:
147168
name="step", # type: ignore
148169
context=current_otel_context, # type: ignore
149170
) as step_span:
171+
if SpanAttributes is not None:
172+
step_span.set_attribute(
173+
SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL"
174+
)
150175
try:
151176
response = await fn(*args, **kwargs)
152177
add_user_attributes(step_span)
153178
add_step_attributes(step_span, response, *args, **kwargs)
179+
154180
return response
155181
except Exception as e:
156182
step_span.set_status(status=StatusCode.ERROR, description=str(e))
@@ -176,6 +202,8 @@ async def trace_async_stream_step_generator(
176202
name="step", # type: ignore
177203
context=current_otel_context, # type: ignore
178204
) as step_span:
205+
if SpanAttributes is not None:
206+
step_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, "GUARDRAIL")
179207
try:
180208
gen = fn(*args, **kwargs)
181209
next_exists = True
@@ -239,6 +267,8 @@ def add_call_attributes(
239267
"args": [sarg for sarg in ser_args if sarg is not None],
240268
"kwargs": {k: v for k, v in ser_kwargs.items() if v is not None},
241269
}
270+
for k in inputs:
271+
inputs[k] = recursive_key_operation(inputs[k], redact)
242272
call_span.set_attribute("input.mime_type", "application/json")
243273
call_span.set_attribute("input.value", json.dumps(inputs))
244274

0 commit comments

Comments
 (0)