From 36ef4ff314c8d33d073f1b1e72daaa5d374bd79c Mon Sep 17 00:00:00 2001 From: abhishek9sharma Date: Sun, 19 Jan 2025 15:59:58 +0800 Subject: [PATCH 01/11] - Implemented redaction functions to obscure sensitive information in strings, - including `redact`, `ismatchingkey`, `can_convert_to_dict`, and - `recursive_key_operation`. - Added unit tests for the new functionalities to ensure correctness and reliability. --- guardrails/telemetry/common.py | 87 +++++++++++++++++++ guardrails/telemetry/runner_tracing.py | 14 ++- .../unit_tests/redaction/test_matching_key.py | 25 ++++++ .../redaction/test_recursive_redaction.py | 49 +++++++++++ tests/unit_tests/redaction/test_redaction.py | 38 ++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/redaction/test_matching_key.py create mode 100644 tests/unit_tests/redaction/test_recursive_redaction.py create mode 100644 tests/unit_tests/redaction/test_redaction.py diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index d2cc53ca8..9d94e8b56 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -124,3 +124,90 @@ def add_user_attributes(span: Span): except Exception as e: logger.warning("Error loading baggage user information", e) pass + + +def redact(value: str) -> str: + """Redacts all but the last four characters of the given string. + + Args: + value (str): The string to be redacted. + + Returns: + str: The redacted string with all but the last four characters + replaced by asterisks. + """ + redaction_length = len(value) - 4 + stars = "*" * redaction_length + return f"{stars}{value[-4:]}" + + +def ismatchingkey( + target_key: str, + keys_to_match: tuple[str, ...] = ("key", "token", "password"), +) -> bool: + """Check if the target key contains any of the specified keys to match. + + Args: + target_key (str): The key to be checked. + keys_to_match (tuple[str, ...], optional): A tuple of keys to match + against the target key. Defaults to ("key", "token"). + + Returns: + bool: True if any of the keys to match are found in the target key, + False otherwise. + """ + for k in keys_to_match: + if k in target_key: + return True + return False + + +def can_convert_to_dict(s): + """Check if a string can be converted to a dictionary. + + This function attempts to load the input string as JSON. If successful, + it returns True, indicating that the string can be converted to a dictionary. + Otherwise, it catches ValueError and TypeError exceptions and returns False. + + Args: + s (str): The input string to be checked. + + Returns: + bool: True if the string can be converted to a dictionary, False otherwise. + """ + try: + json.loads(s) + return True + except (ValueError, TypeError): + return False + + +def recursive_key_operation(data, operation, keys_to_match=["key", "token"]): + """Recursively checks if any key in the dictionary or JSON object is + present in keys_to_match and applies the operation on the corresponding + value. + + Args: + data (dict or list): The dictionary or JSON object to traverse. + keys_to_match (list): List of keys to match. + operation (function): The operation to perform on the matched values. + + Returns: + dict or list: The updated dictionary or JSON object. + """ + if isinstance(data, str) and can_convert_to_dict(data): + data_dict = json.loads(data) + data = str(recursive_key_operation(data_dict, operation, keys_to_match)) + elif isinstance(data, dict): + for key, value in data.items(): + if ismatchingkey(key, keys_to_match) and isinstance(value, str): + # Apply the operation to the value of the matched key + data[key] = operation(value) + else: + # Recursively process nested dictionaries or lists + data[key] = recursive_key_operation(value, operation, keys_to_match) + elif isinstance(data, list): + for i in range(len(data)): + data[i] = recursive_key_operation(data[i], operation, keys_to_match) + + return data diff --git a/guardrails/telemetry/runner_tracing.py b/guardrails/telemetry/runner_tracing.py index 27cf27a97..cbf5b6ac2 100644 --- a/guardrails/telemetry/runner_tracing.py +++ b/guardrails/telemetry/runner_tracing.py @@ -17,7 +17,13 @@ from guardrails.classes.output_type import OT from guardrails.classes.validation_outcome import ValidationOutcome from guardrails.stores.context import get_guard_name -from guardrails.telemetry.common import get_tracer, add_user_attributes, serialize +from guardrails.telemetry.common import ( + get_tracer, + add_user_attributes, + serialize, + recursive_key_operation, + redact, +) from guardrails.utils.safe_get import safe_get from guardrails.version import GUARDRAILS_VERSION @@ -45,10 +51,14 @@ def add_step_attributes( ser_args = [serialize(arg) for arg in args] ser_kwargs = {k: serialize(v) for k, v in kwargs.items()} + inputs = { "args": [sarg for sarg in ser_args if sarg is not None], "kwargs": {k: v for k, v in ser_kwargs.items() if v is not None}, } + for k in inputs: + inputs[k] = recursive_key_operation(inputs[k], redact) + step_span.set_attribute("input.mime_type", "application/json") step_span.set_attribute("input.value", json.dumps(inputs)) @@ -239,6 +249,8 @@ def add_call_attributes( "args": [sarg for sarg in ser_args if sarg is not None], "kwargs": {k: v for k, v in ser_kwargs.items() if v is not None}, } + for k in inputs: + inputs[k] = recursive_key_operation(inputs[k], redact) call_span.set_attribute("input.mime_type", "application/json") call_span.set_attribute("input.value", json.dumps(inputs)) diff --git a/tests/unit_tests/redaction/test_matching_key.py b/tests/unit_tests/redaction/test_matching_key.py new file mode 100644 index 000000000..ff09584ae --- /dev/null +++ b/tests/unit_tests/redaction/test_matching_key.py @@ -0,0 +1,25 @@ +import unittest +from guardrails.telemetry.common import ismatchingkey + + +class TestIsMatchingKey(unittest.TestCase): + def test_key_matches_with_default_keys(self): + self.assertTrue(ismatchingkey("api_key")) + self.assertTrue(ismatchingkey("user_token")) + self.assertFalse(ismatchingkey("username")) + self.assertTrue(ismatchingkey("password")) + + def test_key_matches_with_custom_keys(self): + self.assertTrue(ismatchingkey("api_secret", keys_to_match=("secret",))) + self.assertTrue(ismatchingkey("client_id", keys_to_match=("id",))) + self.assertFalse(ismatchingkey("session", keys_to_match=("key", "token"))) + + def test_empty_key(self): + self.assertFalse(ismatchingkey("", keys_to_match=("key", "token"))) + + def test_empty_keys_to_match(self): + self.assertFalse(ismatchingkey("key", keys_to_match=())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/redaction/test_recursive_redaction.py b/tests/unit_tests/redaction/test_recursive_redaction.py new file mode 100644 index 000000000..aed38d26e --- /dev/null +++ b/tests/unit_tests/redaction/test_recursive_redaction.py @@ -0,0 +1,49 @@ +import unittest +from guardrails.telemetry.common import recursive_key_operation, redact +import ast + + +# Test suite for recursive_key_operation function +class TestRecursiveKeyOperation(unittest.TestCase): + def test_list(self): + data = '{"init_args": [], "init_kwargs": {"model": "gpt-4o-mini", \ + "api_base": "https://api.openai.com/v1", "api_key": "sk-1234"}}' + result = recursive_key_operation(data, redact) + assert ast.literal_eval(result)["init_kwargs"]["api_key"] == "***1234" + + def test_dict_kwargs(self): + data = { + "index": "0", + "api": '{"init_args": [], "init_kwargs": {"model": "gpt-4o-mini",\ + "api_base": "https://api.openai.com/v1", "api_key": "sk-1234"}}', + "messages": None, + "prompt_params": "{}", + "output_schema": '{"type": "string"}', + "output": None, + } + result = recursive_key_operation(data, redact) + assert ast.literal_eval(result["api"])["init_kwargs"]["api_key"] == "***1234" + + def test_nomatch(self): + data = {"somekey": "soemvalue"} + result = recursive_key_operation(data, redact) + self.assertEqual(result, data) + + # def test_empty_dict(self): + # data = {} + # result = recursive_key_operation(data, redact) + # self.assertEqual(result, data) + + # def test_empty_list(self): + # data = [] + # result = recursive_key_operation(data, redact) + # self.assertEqual(result, data) + + # def test_non_string_value(self): + # data = {'key': 123, 'another_key': 'value'} + # result = recursive_key_operation(data, redact) + # self.assertEqual(result, data) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit_tests/redaction/test_redaction.py b/tests/unit_tests/redaction/test_redaction.py new file mode 100644 index 000000000..3475f2cce --- /dev/null +++ b/tests/unit_tests/redaction/test_redaction.py @@ -0,0 +1,38 @@ +import unittest +from guardrails.telemetry.common import redact + + +class TestRedactFunction(unittest.TestCase): + def test_redact_long_string(self): + self.assertEqual(redact("supersecretpassword"), "***************word") + + def test_redact_short_string(self): + self.assertEqual(redact("test"), "test") + + def test_open_ai_example_key(self): + self.assertEqual( + redact("sk-1234abcdefghijklmnopqrstuvwxhp37"), + "*******************************hp37", + ) + + def test_redact_very_short_string(self): + self.assertEqual(redact("abc"), "abc") + + def test_redact_empty_string(self): + self.assertEqual(redact(""), "") + + def test_redact_exact_length(self): + self.assertEqual(redact("1234"), "1234") + + def test_redact_special_characters(self): + self.assertEqual(redact("ab!@#12"), "***@#12") + + def test_redact_single_character(self): + self.assertEqual(redact("a"), "a") + + def test_redact_spaces(self): + self.assertEqual(redact(" test"), "******test") + + +if __name__ == "__main__": + unittest.main() From 8772614a79161eb1317067f3b07dcb8f00851589 Mon Sep 17 00:00:00 2001 From: abhishek9sharma Date: Sun, 19 Jan 2025 16:13:00 +0800 Subject: [PATCH 02/11] fix docstring --- guardrails/telemetry/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index 9d94e8b56..d1335bb3c 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -188,12 +188,12 @@ def recursive_key_operation(data, operation, keys_to_match=["key", "token"]): value. Args: - data (dict or list): The dictionary or JSON object to traverse. + data (dict or list or str): The dictionary or JSON object to traverse. keys_to_match (list): List of keys to match. operation (function): The operation to perform on the matched values. Returns: - dict or list: The updated dictionary or JSON object. + dict or list or str: the modified dictionary, list or string. """ if isinstance(data, str) and can_convert_to_dict(data): data_dict = json.loads(data) From fa8a6567dc958d65b52f406d336e3c8ac34dbe72 Mon Sep 17 00:00:00 2001 From: abhishek9sharma Date: Sun, 19 Jan 2025 19:49:00 +0800 Subject: [PATCH 03/11] - Refactored unit tests for recursive key operations --- .../redaction/test_recursive_redaction.py | 28 +++++++++---------- tests/unit_tests/redaction/test_redaction.py | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit_tests/redaction/test_recursive_redaction.py b/tests/unit_tests/redaction/test_recursive_redaction.py index aed38d26e..587be903a 100644 --- a/tests/unit_tests/redaction/test_recursive_redaction.py +++ b/tests/unit_tests/redaction/test_recursive_redaction.py @@ -29,20 +29,20 @@ def test_nomatch(self): result = recursive_key_operation(data, redact) self.assertEqual(result, data) - # def test_empty_dict(self): - # data = {} - # result = recursive_key_operation(data, redact) - # self.assertEqual(result, data) - - # def test_empty_list(self): - # data = [] - # result = recursive_key_operation(data, redact) - # self.assertEqual(result, data) - - # def test_non_string_value(self): - # data = {'key': 123, 'another_key': 'value'} - # result = recursive_key_operation(data, redact) - # self.assertEqual(result, data) + def test_empty_dict(self): + data = {} + result = recursive_key_operation(data, redact) + self.assertEqual(result, data) + + def test_empty_list(self): + data = [] + result = recursive_key_operation(data, redact) + self.assertEqual(result, data) + + def test_non_string_value(self): + data = {'key': 123, 'another_key': 'value'} + result = recursive_key_operation(data, redact) + self.assertEqual(result, data) if __name__ == "__main__": diff --git a/tests/unit_tests/redaction/test_redaction.py b/tests/unit_tests/redaction/test_redaction.py index 3475f2cce..dda6b7ba2 100644 --- a/tests/unit_tests/redaction/test_redaction.py +++ b/tests/unit_tests/redaction/test_redaction.py @@ -11,8 +11,8 @@ def test_redact_short_string(self): def test_open_ai_example_key(self): self.assertEqual( - redact("sk-1234abcdefghijklmnopqrstuvwxhp37"), - "*******************************hp37", + redact("sk-hp37"), + "***hp37", ) def test_redact_very_short_string(self): From 69bf84a43aba345c7349797f98ef7343c3496fde Mon Sep 17 00:00:00 2001 From: abhishek9sharma Date: Sun, 19 Jan 2025 21:56:39 +0800 Subject: [PATCH 04/11] - Added recursive key operation and redaction for invocation params --- guardrails/telemetry/open_inference.py | 11 ++++++++++- .../unit_tests/redaction/test_recursive_redaction.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/guardrails/telemetry/open_inference.py b/guardrails/telemetry/open_inference.py index 7c58d8a84..f934c12db 100644 --- a/guardrails/telemetry/open_inference.py +++ b/guardrails/telemetry/open_inference.py @@ -1,6 +1,12 @@ from typing import Any, Dict, List, Optional -from guardrails.telemetry.common import get_span, to_dict, serialize +from guardrails.telemetry.common import ( + get_span, + to_dict, + serialize, + recursive_key_operation, + redact, +) def trace_operation( @@ -93,6 +99,9 @@ def trace_llm_call( ser_invocation_parameters = serialize(invocation_parameters) if ser_invocation_parameters: + ser_invocation_parameters = recursive_key_operation( + ser_invocation_parameters, redact + ) current_span.set_attribute( "llm.invocation_parameters", ser_invocation_parameters ) diff --git a/tests/unit_tests/redaction/test_recursive_redaction.py b/tests/unit_tests/redaction/test_recursive_redaction.py index 587be903a..2a252bdf0 100644 --- a/tests/unit_tests/redaction/test_recursive_redaction.py +++ b/tests/unit_tests/redaction/test_recursive_redaction.py @@ -40,7 +40,7 @@ def test_empty_list(self): self.assertEqual(result, data) def test_non_string_value(self): - data = {'key': 123, 'another_key': 'value'} + data = {"key": 123, "another_key": "value"} result = recursive_key_operation(data, redact) self.assertEqual(result, data) From 65dcf5af1ecf47692fa2d32dc5022735bad0e431 Mon Sep 17 00:00:00 2001 From: abhishek9sharma <17585931+abhishek9sharma@users.noreply.github.com> Date: Thu, 23 Jan 2025 20:19:14 +0800 Subject: [PATCH 05/11] - Add type hints to function signatures in `common.py` for improved - code clarity and type safety. - Updated `can_convert_to_dict` and `recursive_key_operation` functions - to specify input and output types. --- guardrails/telemetry/common.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index d1335bb3c..9e3e999cb 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -162,7 +162,7 @@ def ismatchingkey( return False -def can_convert_to_dict(s): +def can_convert_to_dict(s: str) -> bool: """Check if a string can be converted to a dictionary. This function attempts to load the input string as JSON. If successful, @@ -182,15 +182,19 @@ def can_convert_to_dict(s): return False -def recursive_key_operation(data, operation, keys_to_match=["key", "token"]): +def recursive_key_operation( + data: Dict[str, Any] | List[Any] | str, + operation: Callable[[str], str], + keys_to_match: List[str] = ["key", "token"], +) -> Dict[str, Any] | List[Any] | str: """Recursively checks if any key in the dictionary or JSON object is present in keys_to_match and applies the operation on the corresponding value. Args: data (dict or list or str): The dictionary or JSON object to traverse. - keys_to_match (list): List of keys to match. operation (function): The operation to perform on the matched values. + keys_to_match (list): List of keys to match. Returns: dict or list or str: the modified dictionary, list or string. From 1b59ded60a061430eb2482b9027125ced83eb86f Mon Sep 17 00:00:00 2001 From: Caleb Courier <13314870+CalebCourier@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:02:12 -0600 Subject: [PATCH 06/11] Reserialize redacted parameters to strings --- guardrails/telemetry/open_inference.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/guardrails/telemetry/open_inference.py b/guardrails/telemetry/open_inference.py index f934c12db..dde12487d 100644 --- a/guardrails/telemetry/open_inference.py +++ b/guardrails/telemetry/open_inference.py @@ -98,12 +98,18 @@ def trace_llm_call( ) ser_invocation_parameters = serialize(invocation_parameters) - if ser_invocation_parameters: - ser_invocation_parameters = recursive_key_operation( - ser_invocation_parameters, redact - ) + redacted_ser_invocation_parameters = recursive_key_operation( + ser_invocation_parameters, redact + ) + reser_invocation_parameters = ( + json.dumps(redacted_ser_invocation_parameters) + if isinstance(redacted_ser_invocation_parameters, dict) + or isinstance(redacted_ser_invocation_parameters, list) + else redacted_ser_invocation_parameters + ) + if reser_invocation_parameters: current_span.set_attribute( - "llm.invocation_parameters", ser_invocation_parameters + "llm.invocation_parameters", reser_invocation_parameters ) ser_model_name = serialize(model_name) From c2c95db0394721a1779fe27dd1750f003743bbef Mon Sep 17 00:00:00 2001 From: Caleb Courier <13314870+CalebCourier@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:10:46 -0600 Subject: [PATCH 07/11] Update guardrails/telemetry/open_inference.py --- guardrails/telemetry/open_inference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/guardrails/telemetry/open_inference.py b/guardrails/telemetry/open_inference.py index dde12487d..1fc33e08e 100644 --- a/guardrails/telemetry/open_inference.py +++ b/guardrails/telemetry/open_inference.py @@ -1,3 +1,4 @@ +import json from typing import Any, Dict, List, Optional from guardrails.telemetry.common import ( From a74b07bc5baf7fca8d398cd42ed50fcf0a522873 Mon Sep 17 00:00:00 2001 From: Caleb Courier <13314870+CalebCourier@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:17:17 -0600 Subject: [PATCH 08/11] cast keys_to_match to tuple --- guardrails/telemetry/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index 9e3e999cb..38525d0bc 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -204,7 +204,7 @@ def recursive_key_operation( data = str(recursive_key_operation(data_dict, operation, keys_to_match)) elif isinstance(data, dict): for key, value in data.items(): - if ismatchingkey(key, keys_to_match) and isinstance(value, str): + if ismatchingkey(key, tuple(keys_to_match)) and isinstance(value, str): # Apply the operation to the value of the matched key data[key] = operation(value) else: From a5795f11ec0b38a60b0d6169f28ef37f06da0aac Mon Sep 17 00:00:00 2001 From: Caleb Courier <13314870+CalebCourier@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:17:40 -0600 Subject: [PATCH 09/11] Use older Union syntax for older python version support --- guardrails/telemetry/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index 38525d0bc..76102b3d1 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -186,7 +186,7 @@ def recursive_key_operation( data: Dict[str, Any] | List[Any] | str, operation: Callable[[str], str], keys_to_match: List[str] = ["key", "token"], -) -> Dict[str, Any] | List[Any] | str: +) -> Optional[Union[Dict[str, Any], List[Any], str]]: """Recursively checks if any key in the dictionary or JSON object is present in keys_to_match and applies the operation on the corresponding value. From 227acbe38749d8e691a030449a40022e6a299687 Mon Sep 17 00:00:00 2001 From: Caleb Courier <13314870+CalebCourier@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:17:47 -0600 Subject: [PATCH 10/11] Use older Union syntax for older python version support --- guardrails/telemetry/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index 76102b3d1..a3fb8392f 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -183,7 +183,7 @@ def can_convert_to_dict(s: str) -> bool: def recursive_key_operation( - data: Dict[str, Any] | List[Any] | str, + data: Optional[Union[Dict[str, Any], List[Any], str]], operation: Callable[[str], str], keys_to_match: List[str] = ["key", "token"], ) -> Optional[Union[Dict[str, Any], List[Any], str]]: From 524856ff9ef77da1851568a06f5936d2d66fd4c9 Mon Sep 17 00:00:00 2001 From: abhishek9sharma <17585931+abhishek9sharma@users.noreply.github.com> Date: Fri, 24 Jan 2025 07:07:59 +0800 Subject: [PATCH 11/11] - Fix imports - Updated docstring for clarity and improved input handling. --- guardrails/telemetry/common.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/guardrails/telemetry/common.py b/guardrails/telemetry/common.py index a3fb8392f..4b647b673 100644 --- a/guardrails/telemetry/common.py +++ b/guardrails/telemetry/common.py @@ -1,5 +1,5 @@ import json -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Union, List from opentelemetry.baggage import get_baggage from opentelemetry import context from opentelemetry.context import Context @@ -185,19 +185,30 @@ def can_convert_to_dict(s: str) -> bool: def recursive_key_operation( data: Optional[Union[Dict[str, Any], List[Any], str]], operation: Callable[[str], str], - keys_to_match: List[str] = ["key", "token"], + keys_to_match: List[str] = ["key", "token", "password"], ) -> Optional[Union[Dict[str, Any], List[Any], str]]: - """Recursively checks if any key in the dictionary or JSON object is - present in keys_to_match and applies the operation on the corresponding - value. + """Recursively traverses a dictionary, list, or JSON string and applies a + specified operation to the values of keys that match any in the + `keys_to_match` list. This function is useful for masking sensitive data + (e.g., keys, tokens, passwords) in nested structures. Args: - data (dict or list or str): The dictionary or JSON object to traverse. - operation (function): The operation to perform on the matched values. - keys_to_match (list): List of keys to match. + data (Optional[Union[Dict[str, Any], List[Any], str]]): The input data + to traverse. This can bea dictionary, list, or JSON string. If a + JSON string is provided, it will be parsed into a dictionary before + processing. + + operation (Callable[[str], str]): A function that takes a string value + and returns a modified string. This operation is applied to the values + of keys that match any in `keys_to_match`. + keys_to_match (List[str]): A list of keys to search for in the data. If + a key matche any in this list, the corresponding value will be processed + by the `operation`. Defaults to ["key", "token", "password"]. Returns: - dict or list or str: the modified dictionary, list or string. + Optional[Union[Dict[str, Any], List[Any], str]]: The modified data structure + with the operation applied to the values of matched keys. The return type + matches the input type (dict, list, or str). """ if isinstance(data, str) and can_convert_to_dict(data): data_dict = json.loads(data)