Skip to main content

adk_agent/
guardrails.rs

1//! Guardrail integration for LlmAgent
2//!
3//! This module provides guardrail support when the `guardrails` feature is enabled.
4
5use adk_core::{Content, Result};
6
7#[cfg(feature = "guardrails")]
8use adk_core::AdkError;
9
10#[cfg(feature = "guardrails")]
11pub use adk_guardrail::{
12    ContentFilter, ContentFilterConfig, Guardrail, GuardrailExecutor, GuardrailResult,
13    GuardrailSet, PiiRedactor, PiiType, Severity,
14};
15
16#[cfg(feature = "guardrails")]
17pub use adk_guardrail::SchemaValidator;
18
19/// Placeholder type when guardrails feature is disabled
20#[cfg(not(feature = "guardrails"))]
21pub struct GuardrailSet;
22
23#[cfg(not(feature = "guardrails"))]
24impl GuardrailSet {
25    /// Create an empty guardrail set (no-op when feature is disabled).
26    pub fn new() -> Self {
27        Self
28    }
29    /// Returns `true` (always empty when feature is disabled).
30    pub fn is_empty(&self) -> bool {
31        true
32    }
33}
34
35#[cfg(not(feature = "guardrails"))]
36impl Default for GuardrailSet {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42#[cfg(feature = "guardrails")]
43pub(crate) async fn enforce_guardrails(
44    guardrails: &GuardrailSet,
45    content: &Content,
46    phase: &str,
47) -> Result<Content> {
48    let result = GuardrailExecutor::run(guardrails, content)
49        .await
50        .map_err(|err| AdkError::agent(format!("{phase} guardrail failed: {err}")))?;
51
52    if !result.passed {
53        let failures = result
54            .failures
55            .iter()
56            .map(|(name, reason, severity)| format!("{name} ({severity:?}): {reason}"))
57            .collect::<Vec<_>>()
58            .join("; ");
59        return Err(AdkError::agent(format!("{phase} guardrails blocked content: {failures}")));
60    }
61
62    Ok(result.transformed_content.unwrap_or_else(|| content.clone()))
63}
64
65#[cfg(not(feature = "guardrails"))]
66pub(crate) async fn enforce_guardrails(
67    _guardrails: &GuardrailSet,
68    content: &Content,
69    _phase: &str,
70) -> Result<Content> {
71    Ok(content.clone())
72}
73
74#[cfg(feature = "guardrails")]
75pub use adk_guardrail::{
76    DeniedArgumentPattern, PathAllowList, ToolCallDecision, ToolGuardrail, ToolGuardrailResult,
77    ToolGuardrailSet,
78};
79
80/// Placeholder type when the guardrails feature is disabled.
81#[cfg(not(feature = "guardrails"))]
82pub struct ToolGuardrailSet;
83
84#[cfg(not(feature = "guardrails"))]
85impl ToolGuardrailSet {
86    /// Create an empty tool guardrail set (no-op when the feature is disabled).
87    pub fn new() -> Self {
88        Self
89    }
90    /// Returns `true` (always empty when the feature is disabled).
91    pub fn is_empty(&self) -> bool {
92        true
93    }
94}
95
96#[cfg(not(feature = "guardrails"))]
97impl Default for ToolGuardrailSet {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103/// Outcome of screening a tool call, independent of whether guardrails are compiled in.
104pub(crate) enum ToolScreening {
105    /// The call may proceed with these arguments.
106    Allow(serde_json::Value),
107    /// The call is refused, with a reason to report back to the model.
108    ///
109    /// Never constructed without the `guardrails` feature, where screening is a no-op that always
110    /// allows. The variant still exists so the call site is identical in both builds.
111    #[cfg_attr(not(feature = "guardrails"), allow(dead_code))]
112    Deny(String),
113}
114
115/// Screens a tool call against `guardrails` before it executes.
116#[cfg(feature = "guardrails")]
117pub(crate) async fn screen_tool_call(
118    guardrails: &ToolGuardrailSet,
119    tool_name: &str,
120    args: &serde_json::Value,
121) -> ToolScreening {
122    if guardrails.is_empty() {
123        return ToolScreening::Allow(args.clone());
124    }
125
126    match guardrails.evaluate(tool_name, args).await {
127        ToolCallDecision::Allow { args } => ToolScreening::Allow(args),
128        ToolCallDecision::Deny { guardrail, reason, severity } => ToolScreening::Deny(format!(
129            "Tool '{tool_name}' blocked by guardrail '{guardrail}' ({severity:?}): {reason}"
130        )),
131    }
132}
133
134#[cfg(not(feature = "guardrails"))]
135pub(crate) async fn screen_tool_call(
136    _guardrails: &ToolGuardrailSet,
137    _tool_name: &str,
138    args: &serde_json::Value,
139) -> ToolScreening {
140    ToolScreening::Allow(args.clone())
141}