diff --git a/litellm/proxy/guardrails/guardrail_hooks/privaite/engine.py b/litellm/proxy/guardrails/guardrail_hooks/privaite/engine.py index 419077cbc61..3cc873f7b91 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/privaite/engine.py +++ b/litellm/proxy/guardrails/guardrail_hooks/privaite/engine.py @@ -65,9 +65,11 @@ def _module(name: str) -> ModuleType: def _factory(module: ModuleType, name: str) -> Callable[..., object]: value: Final = getattr(module, name, None) - if not callable(value): - raise TypeError(f"{module.__name__}.{name} is missing. {INSTALL_HINT}") - return value + if callable(value): + return value + if hasattr(module, name): + raise TypeError(f"{module.__name__}.{name} is not callable. {INSTALL_HINT}") + raise RuntimeError(f"{module.__name__}.{name} is missing. {INSTALL_HINT}") def _narrow(value: object, kind: type[_Narrowed]) -> _Narrowed: diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py b/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py index 8e69b9d2268..6fb70e02171 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/test_privaite.py @@ -1386,3 +1386,41 @@ async def test_streaming_tool_arguments_stay_valid_json_with_special_chars(monke assert json.loads(tool_args) == {"name": _SPECIALS_ORIGINAL} assert json.loads(fc_args) == {"name": _SPECIALS_ORIGINAL} assert content == f"Hi {_SPECIALS_ORIGINAL}" + + +@pytest.mark.asyncio +async def test_pre_call_scans_a_responses_item_whose_type_is_not_a_string(): + gr = _make_guardrail() + data = {"input": [{"type": 5, "output": "Email Marie Dupont"}]} + + out = await gr.async_pre_call_hook(None, None, data, "aresponses") + + assert out["input"][0]["output"] == "Email " + + +@pytest.mark.asyncio +async def test_pre_call_stashes_the_map_when_metadata_is_null(): + gr = _make_guardrail() + data = {"messages": [{"role": "user", "content": "Email Marie Dupont"}], "metadata": None} + + out = await gr.async_pre_call_hook(None, None, data, "acompletion") + + assert out["metadata"]["privaite_map"] == {"": "Marie Dupont"} + + +@pytest.mark.asyncio +async def test_post_call_restores_a_dict_shaped_response(): + gr = _make_guardrail() + response = {"choices": [{"message": {"content": "Sending to "}}]} + + out = await gr.async_post_call_success_hook({"metadata": {"privaite_map": _FAKES}}, None, response) + + assert out["choices"][0]["message"]["content"] == "Sending to Marie Dupont" + + +@pytest.mark.asyncio +async def test_engine_for_a_privaite_without_pii_engine_recommends_the_install(monkeypatch): + monkeypatch.delattr(sys.modules["privaite.pii.engine"], "PIIEngine") + + with pytest.raises(RuntimeError, match=r"pip install 'privaite>=0\.7\.2'"): + await _make_guardrail()._engine_for(["en"])