Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions docs/benchmarking/nsfw.md

This file was deleted.

20 changes: 16 additions & 4 deletions src/guardrails/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ def __new__(
cls,
config: str | Path | dict[str, Any],
name: str,
instructions: str,
instructions: str | Callable[[Any, Any], Any] | None = None,
raise_guardrail_errors: bool = False,
block_on_tool_violations: bool = False,
**agent_kwargs: Any,
Expand All @@ -511,7 +511,9 @@ def __new__(
Args:
config: Pipeline configuration (file path, dict, or JSON string)
name: Agent name
instructions: Agent instructions
instructions: Agent instructions. Can be a string, a callable that dynamically
generates instructions, or None. If a callable, it will receive the context
and agent instance and must return a string.
raise_guardrail_errors: If True, raise exceptions when guardrails fail to execute.
If False (default), treat guardrail errors as safe and continue execution.
block_on_tool_violations: If True, tool guardrail violations raise exceptions (halt execution).
Expand Down Expand Up @@ -553,7 +555,11 @@ def __new__(
input_tool, input_agent = _separate_tool_level_from_agent_level(stage_guardrails.get("input", []))
output_tool, output_agent = _separate_tool_level_from_agent_level(stage_guardrails.get("output", []))

# Create agent-level INPUT guardrails
# Extract any user-provided guardrails from agent_kwargs
user_input_guardrails = agent_kwargs.pop("input_guardrails", [])
user_output_guardrails = agent_kwargs.pop("output_guardrails", [])

# Create agent-level INPUT guardrails from config
input_guardrails = []

# Add agent-level guardrails from pre_flight and input stages
Expand All @@ -573,7 +579,10 @@ def __new__(
)
)

# Create agent-level OUTPUT guardrails
# Merge with user-provided input guardrails (config ones run first, then user ones)
input_guardrails.extend(user_input_guardrails)

# Create agent-level OUTPUT guardrails from config
output_guardrails = []
if output_agent:
output_guardrails = _create_agents_guardrails_from_config(
Expand All @@ -583,6 +592,9 @@ def __new__(
raise_guardrail_errors=raise_guardrail_errors,
)

# Merge with user-provided output guardrails (config ones run first, then user ones)
output_guardrails.extend(user_output_guardrails)

# Apply tool-level guardrails
tools = agent_kwargs.get("tools", [])

Expand Down
137 changes: 137 additions & 0 deletions tests/unit/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,140 @@ def test_guardrail_agent_without_tools(monkeypatch: pytest.MonkeyPatch) -> None:
agent_instance = agents.GuardrailAgent(config={}, name="NoTools", instructions="None")

assert getattr(agent_instance, "input_guardrails", []) == [] # noqa: S101


def test_guardrail_agent_without_instructions(monkeypatch: pytest.MonkeyPatch) -> None:
"""GuardrailAgent should work without instructions parameter."""
pipeline = SimpleNamespace(pre_flight=None, input=None, output=None)

monkeypatch.setattr(runtime_module, "load_pipeline_bundles", lambda config: pipeline, raising=False)
monkeypatch.setattr(runtime_module, "instantiate_guardrails", lambda *args, **kwargs: [], raising=False)

# Should not raise TypeError about missing instructions
agent_instance = agents.GuardrailAgent(config={}, name="NoInstructions")

assert isinstance(agent_instance, agents_module.Agent) # noqa: S101
assert agent_instance.instructions is None # noqa: S101


def test_guardrail_agent_with_callable_instructions(monkeypatch: pytest.MonkeyPatch) -> None:
"""GuardrailAgent should accept callable instructions."""
pipeline = SimpleNamespace(pre_flight=None, input=None, output=None)

monkeypatch.setattr(runtime_module, "load_pipeline_bundles", lambda config: pipeline, raising=False)
monkeypatch.setattr(runtime_module, "instantiate_guardrails", lambda *args, **kwargs: [], raising=False)

def dynamic_instructions(ctx: Any, agent: Any) -> str:
return f"You are {agent.name}"

agent_instance = agents.GuardrailAgent(
config={},
name="DynamicAgent",
instructions=dynamic_instructions,
)

assert isinstance(agent_instance, agents_module.Agent) # noqa: S101
assert callable(agent_instance.instructions) # noqa: S101
assert agent_instance.instructions == dynamic_instructions # noqa: S101


def test_guardrail_agent_merges_user_input_guardrails(monkeypatch: pytest.MonkeyPatch) -> None:
"""User input guardrails should be merged with config guardrails."""
agent_guard = _make_guardrail("Config Input Guard")

class FakePipeline:
def __init__(self) -> None:
self.pre_flight = None
self.input = SimpleNamespace()
self.output = None

pipeline = FakePipeline()

def fake_load_pipeline_bundles(config: Any) -> FakePipeline:
return pipeline

def fake_instantiate_guardrails(stage: Any, registry: Any | None = None) -> list[Any]:
if stage is pipeline.input:
return [agent_guard]
return []

from guardrails import runtime as runtime_module

monkeypatch.setattr(runtime_module, "load_pipeline_bundles", fake_load_pipeline_bundles)
monkeypatch.setattr(runtime_module, "instantiate_guardrails", fake_instantiate_guardrails)

# Create a custom user guardrail
custom_guardrail = lambda ctx, agent, input: None # noqa: E731

agent_instance = agents.GuardrailAgent(
config={},
name="MergedAgent",
instructions="Test",
input_guardrails=[custom_guardrail],
)

# Should have both config and user guardrails merged
assert isinstance(agent_instance, agents_module.Agent) # noqa: S101
assert len(agent_instance.input_guardrails) == 2 # noqa: S101
# Config guardrail from _create_agents_guardrails_from_config, then user guardrail


def test_guardrail_agent_merges_user_output_guardrails(monkeypatch: pytest.MonkeyPatch) -> None:
"""User output guardrails should be merged with config guardrails."""
agent_guard = _make_guardrail("Config Output Guard")

class FakePipeline:
def __init__(self) -> None:
self.pre_flight = None
self.input = None
self.output = SimpleNamespace()

pipeline = FakePipeline()

def fake_load_pipeline_bundles(config: Any) -> FakePipeline:
return pipeline

def fake_instantiate_guardrails(stage: Any, registry: Any | None = None) -> list[Any]:
if stage is pipeline.output:
return [agent_guard]
return []

from guardrails import runtime as runtime_module

monkeypatch.setattr(runtime_module, "load_pipeline_bundles", fake_load_pipeline_bundles)
monkeypatch.setattr(runtime_module, "instantiate_guardrails", fake_instantiate_guardrails)

# Create a custom user guardrail
custom_guardrail = lambda ctx, agent, output: None # noqa: E731

agent_instance = agents.GuardrailAgent(
config={},
name="MergedAgent",
instructions="Test",
output_guardrails=[custom_guardrail],
)

# Should have both config and user guardrails merged
assert isinstance(agent_instance, agents_module.Agent) # noqa: S101
assert len(agent_instance.output_guardrails) == 2 # noqa: S101
# Config guardrail from _create_agents_guardrails_from_config, then user guardrail


def test_guardrail_agent_with_empty_user_guardrails(monkeypatch: pytest.MonkeyPatch) -> None:
"""GuardrailAgent should handle empty user guardrail lists gracefully."""
pipeline = SimpleNamespace(pre_flight=None, input=None, output=None)

monkeypatch.setattr(runtime_module, "load_pipeline_bundles", lambda config: pipeline, raising=False)
monkeypatch.setattr(runtime_module, "instantiate_guardrails", lambda *args, **kwargs: [], raising=False)

agent_instance = agents.GuardrailAgent(
config={},
name="EmptyListAgent",
instructions="Test",
input_guardrails=[],
output_guardrails=[],
)

assert isinstance(agent_instance, agents_module.Agent) # noqa: S101
assert agent_instance.input_guardrails == [] # noqa: S101
assert agent_instance.output_guardrails == [] # noqa: S101
Loading